Commit c95c379
4
files
+530
added
-6
deleted
@@ -27,11 +27,11 @@ pub use event::{
27
27
pub use geometry::{Insets, Point, Rect, Size};
28
28
pub use scale::Scale;
29
29
pub use style::{
30
- ActionRowMetrics, AvatarMetrics, BannerMetrics, BottomSheetMetrics, ButtonMetrics, CardMetrics,
31
- CheckMetrics, DropDownMetrics, EntryMetrics, GroupMetrics, HeaderBarMetrics, ItemViewMetrics,
32
- MenuMetrics, Metrics, NavMetrics, PagerMetrics, PathBarMetrics, RadioMetrics, Radius,
33
- ScrollViewMetrics, SegmentedMetrics, ShortcutMetrics, SliderMetrics, Spacing, SpinnerMetrics,
34
- SplitMetrics, StatusPageMetrics, StepperMetrics, SwitchMetrics, TabsMetrics, Theme,
35
- ToastMetrics, TooltipMetrics, Typography,
30
+ ActionRowMetrics, AvatarMetrics, BannerMetrics, BarMetrics, BottomSheetMetrics, ButtonMetrics,
31
+ CardMetrics, CheckMetrics, DropDownMetrics, EntryMetrics, GroupMetrics, HeaderBarMetrics,
32
+ ItemViewMetrics, MenuMetrics, Metrics, NavMetrics, PagerMetrics, PathBarMetrics, RadioMetrics,
33
+ Radius, ScrollViewMetrics, SegmentedMetrics, ShortcutMetrics, SliderMetrics, Spacing,
34
+ SpinnerMetrics, SplitMetrics, StatusPageMetrics, StepperMetrics, SwitchMetrics, TabsMetrics,
35
+ Theme, ToastMetrics, TooltipMetrics, Typography,
36
36
};
37
37
pub use theme_file::{available as available_themes, StyledTheme, ThemePatch};
@@ -552,6 +552,15 @@ pub struct GroupMetrics {
552
552
pub pad_x: f64,
553
553
}
554
554
555
+/// Geometria pasków (postęp, poziom).
556
+#[derive(Debug, Clone, Copy, PartialEq)]
557
+pub struct BarMetrics {
558
+ /// Grubość paska.
559
+ pub thickness: f64,
560
+ /// Promień zaokrąglenia.
561
+ pub radius: f64,
562
+}
563
+
555
564
/// Geometria kontrolek („component metrics”) — rozmiary i paddingi w pikselach logicznych.
556
565
///
557
566
/// Widgety nie mają już własnych stałych rozmiarów: czytają je z motywu, dzięki czemu
@@ -588,6 +597,7 @@ pub struct Metrics {
588
597
pub shortcut: ShortcutMetrics,
589
598
pub split: SplitMetrics,
590
599
pub group: GroupMetrics,
600
+ pub bar: BarMetrics,
591
601
}
592
602
593
603
impl Metrics {
@@ -750,6 +760,10 @@ impl Metrics {
750
760
gap: 18.0,
751
761
pad_x: 12.0,
752
762
},
763
+ bar: BarMetrics {
764
+ thickness: 8.0,
765
+ radius: 4.0,
766
+ },
753
767
}
754
768
}
755
769
}
756
770
new file mode 100644
@@ -0,0 +1,503 @@
1
+//! Widgety prezentacji (Display): `Label`, `Separator`, `ProgressBar`, `LevelBar`, `Frame`,
2
+//! `Revealer`, `Overlay`, `AspectFrame`.
3
+
4
+use libluna_core::style::weight;
5
+use libluna_core::{Rect, Size, Theme};
6
+use libluna_render::TextEngine;
7
+
8
+use crate::bin::BinAlign;
9
+use crate::ctx::PaintCtx;
10
+
11
+/// Orientacja (pozioma/pionowa).
12
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13
+pub enum Orientation {
14
+ #[default]
15
+ Horizontal,
16
+ Vertical,
17
+}
18
+
19
+/// Wyrównanie napisu w poziomie.
20
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21
+pub enum LabelAlign {
22
+ Start,
23
+ #[default]
24
+ Center,
25
+ End,
26
+}
27
+
28
+/// Etykieta tekstowa (z ucinaniem w wielokropek).
29
+#[derive(Debug, Clone, Default)]
30
+pub struct Label {
31
+ text: String,
32
+ align: LabelAlign,
33
+ dim: bool,
34
+ bold: bool,
35
+}
36
+
37
+impl Label {
38
+ pub fn new(text: impl Into<String>) -> Self {
39
+ Self {
40
+ text: text.into(),
41
+ ..Self::default()
42
+ }
43
+ }
44
+
45
+ pub fn align(mut self, align: LabelAlign) -> Self {
46
+ self.align = align;
47
+ self
48
+ }
49
+
50
+ pub fn dim(mut self, dim: bool) -> Self {
51
+ self.dim = dim;
52
+ self
53
+ }
54
+
55
+ pub fn bold(mut self, bold: bool) -> Self {
56
+ self.bold = bold;
57
+ self
58
+ }
59
+
60
+ pub fn set_text(&mut self, text: impl Into<String>) {
61
+ self.text = text.into();
62
+ }
63
+
64
+ pub fn text(&self) -> &str {
65
+ &self.text
66
+ }
67
+
68
+ pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
69
+ let theme = *ctx.theme;
70
+ let size = theme.typography.body.size;
71
+ let w = if self.bold {
72
+ weight::BOLD
73
+ } else {
74
+ weight::NORMAL
75
+ };
76
+ let color = if self.dim {
77
+ theme.colors.dim_text
78
+ } else {
79
+ theme.colors.text
80
+ };
81
+ let cy = rect.center().y;
82
+ ctx.scene.push_clip(rect);
83
+ match self.align {
84
+ LabelAlign::Start => ctx.label_left(rect.x(), cy, &self.text, size, w, color),
85
+ LabelAlign::Center => ctx.label_center(rect.center().x, cy, &self.text, size, w, color),
86
+ LabelAlign::End => ctx.label_right(rect.right(), cy, &self.text, size, w, color),
87
+ }
88
+ ctx.scene.pop_clip();
89
+ }
90
+}
91
+
92
+/// Separator (linia).
93
+#[derive(Debug, Clone, Copy)]
94
+pub struct Separator {
95
+ orientation: Orientation,
96
+}
97
+
98
+impl Separator {
99
+ pub fn new(orientation: Orientation) -> Self {
100
+ Self { orientation }
101
+ }
102
+
103
+ pub fn horizontal() -> Self {
104
+ Self::new(Orientation::Horizontal)
105
+ }
106
+
107
+ pub fn vertical() -> Self {
108
+ Self::new(Orientation::Vertical)
109
+ }
110
+
111
+ pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
112
+ let theme = *ctx.theme;
113
+ let line = match self.orientation {
114
+ Orientation::Horizontal => {
115
+ Rect::from_xywh(rect.x(), rect.center().y - 0.5, rect.width(), 1.0)
116
+ }
117
+ Orientation::Vertical => {
118
+ Rect::from_xywh(rect.center().x - 0.5, rect.y(), 1.0, rect.height())
119
+ }
120
+ };
121
+ ctx.scene.fill_rect(line, theme.colors.border);
122
+ }
123
+}
124
+
125
+/// Pasek postępu (0..1) z opcjonalnym napisem.
126
+#[derive(Debug, Clone, Default)]
127
+pub struct ProgressBar {
128
+ fraction: f64,
129
+ text: Option<String>,
130
+ show_text: bool,
131
+}
132
+
133
+impl ProgressBar {
134
+ pub fn new() -> Self {
135
+ Self::default()
136
+ }
137
+
138
+ pub fn fraction(mut self, fraction: f64) -> Self {
139
+ self.fraction = fraction.clamp(0.0, 1.0);
140
+ self
141
+ }
142
+
143
+ pub fn set_fraction(&mut self, fraction: f64) {
144
+ self.fraction = fraction.clamp(0.0, 1.0);
145
+ }
146
+
147
+ pub fn text(mut self, text: impl Into<String>) -> Self {
148
+ self.text = Some(text.into());
149
+ self.show_text = true;
150
+ self
151
+ }
152
+
153
+ pub fn show_text(mut self, show: bool) -> Self {
154
+ self.show_text = show;
155
+ self
156
+ }
157
+
158
+ pub fn fraction_value(&self) -> f64 {
159
+ self.fraction
160
+ }
161
+
162
+ pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
163
+ let theme = *ctx.theme;
164
+ let m = theme.metrics.bar;
165
+ let h = rect.height().min(m.thickness);
166
+ let track = Rect::from_xywh(rect.x(), rect.center().y - h / 2.0, rect.width(), h);
167
+ ctx.scene
168
+ .fill_rounded_rect(track, m.radius, theme.colors.shade);
169
+ let fill = Rect::from_xywh(
170
+ track.x(),
171
+ track.y(),
172
+ track.width() * self.fraction,
173
+ track.height(),
174
+ );
175
+ if self.fraction > 0.0 {
176
+ ctx.scene
177
+ .fill_rounded_rect(fill, m.radius, theme.colors.accent);
178
+ }
179
+ if self.show_text {
180
+ if let Some(text) = &self.text {
181
+ ctx.label_center(
182
+ rect.center().x,
183
+ rect.center().y,
184
+ text,
185
+ theme.typography.caption.size,
186
+ weight::NORMAL,
187
+ theme.colors.text,
188
+ );
189
+ } else {
190
+ let label = format!("{}%", (self.fraction * 100.0).round() as i32);
191
+ ctx.label_center(
192
+ rect.center().x,
193
+ rect.center().y,
194
+ &label,
195
+ theme.typography.caption.size,
196
+ weight::NORMAL,
197
+ theme.colors.text,
198
+ );
199
+ }
200
+ }
201
+ }
202
+}
203
+
204
+/// Wskaźnik poziomu (segmented).
205
+#[derive(Debug, Clone, Copy)]
206
+pub struct LevelBar {
207
+ value: f64,
208
+ segments: usize,
209
+}
210
+
211
+impl Default for LevelBar {
212
+ fn default() -> Self {
213
+ Self {
214
+ value: 0.0,
215
+ segments: 8,
216
+ }
217
+ }
218
+}
219
+
220
+impl LevelBar {
221
+ pub fn new() -> Self {
222
+ Self::default()
223
+ }
224
+
225
+ pub fn value(mut self, value: f64) -> Self {
226
+ self.value = value.clamp(0.0, 1.0);
227
+ self
228
+ }
229
+
230
+ pub fn segments(mut self, segments: usize) -> Self {
231
+ self.segments = segments.max(1);
232
+ self
233
+ }
234
+
235
+ pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
236
+ let theme = *ctx.theme;
237
+ let n = self.segments;
238
+ let gap = 2.0;
239
+ let seg_w = (rect.width() - gap * (n as f64 - 1.0)).max(0.0) / n as f64;
240
+ let filled = (self.value * n as f64).round() as usize;
241
+ for i in 0..n {
242
+ let x = rect.x() + i as f64 * (seg_w + gap);
243
+ let seg = Rect::from_xywh(x, rect.y(), seg_w, rect.height());
244
+ let color = if i < filled {
245
+ theme.colors.accent
246
+ } else {
247
+ theme.colors.shade
248
+ };
249
+ ctx.scene.fill_rounded_rect(seg, 2.0, color);
250
+ }
251
+ }
252
+}
253
+
254
+/// Ramka dekoracyjna z opcjonalną etykietą.
255
+#[derive(Debug, Clone, Default)]
256
+pub struct Frame {
257
+ label: Option<String>,
258
+}
259
+
260
+impl Frame {
261
+ pub fn new() -> Self {
262
+ Self::default()
263
+ }
264
+
265
+ pub fn label(mut self, label: impl Into<String>) -> Self {
266
+ self.label = Some(label.into());
267
+ self
268
+ }
269
+
270
+ /// Prostokąt zawartości (wewnątrz ramki).
271
+ pub fn content_rect(&self, rect: Rect, theme: &Theme) -> Rect {
272
+ rect.deflate(libluna_core::Insets::splat(theme.spacing.md))
273
+ }
274
+
275
+ pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) -> Rect {
276
+ let theme = *ctx.theme;
277
+ ctx.scene
278
+ .stroke_rounded_rect(rect, theme.radius.medium, 1.0, theme.colors.border);
279
+ if let Some(label) = &self.label {
280
+ ctx.label_left(
281
+ rect.x() + theme.spacing.md,
282
+ rect.y(),
283
+ label,
284
+ theme.typography.caption.size,
285
+ weight::BOLD,
286
+ theme.colors.dim_text,
287
+ );
288
+ }
289
+ self.content_rect(rect, &theme)
290
+ }
291
+}
292
+
293
+/// Odkrywanie/ukrywanie zawartości (bez animacji — ułamek zadaje aplikacja).
294
+#[derive(Debug, Clone, Copy)]
295
+pub struct Revealer {
296
+ open: bool,
297
+ fraction: f64,
298
+ orientation: Orientation,
299
+}
300
+
301
+impl Default for Revealer {
302
+ fn default() -> Self {
303
+ Self {
304
+ open: false,
305
+ fraction: 0.0,
306
+ orientation: Orientation::Vertical,
307
+ }
308
+ }
309
+}
310
+
311
+impl Revealer {
312
+ pub fn new() -> Self {
313
+ Self::default()
314
+ }
315
+
316
+ pub fn orientation(mut self, orientation: Orientation) -> Self {
317
+ self.orientation = orientation;
318
+ self
319
+ }
320
+
321
+ pub fn set_open(&mut self, open: bool) {
322
+ self.open = open;
323
+ self.fraction = if open { 1.0 } else { 0.0 };
324
+ }
325
+
326
+ pub fn set_fraction(&mut self, fraction: f64) {
327
+ self.fraction = fraction.clamp(0.0, 1.0);
328
+ self.open = self.fraction > 0.0;
329
+ }
330
+
331
+ pub fn is_open(&self) -> bool {
332
+ self.open
333
+ }
334
+
335
+ /// Widoczny prostokąt zawartości (przycięty wg ułamka).
336
+ pub fn reveal_rect(&self, rect: Rect) -> Rect {
337
+ match self.orientation {
338
+ Orientation::Vertical => Rect::from_xywh(
339
+ rect.x(),
340
+ rect.y(),
341
+ rect.width(),
342
+ rect.height() * self.fraction,
343
+ ),
344
+ Orientation::Horizontal => Rect::from_xywh(
345
+ rect.x(),
346
+ rect.y(),
347
+ rect.width() * self.fraction,
348
+ rect.height(),
349
+ ),
350
+ }
351
+ }
352
+}
353
+
354
+/// Nakładka: prostokąt główny + pozycjonowanie dzieci „overlay”.
355
+#[derive(Debug, Clone, Copy, Default)]
356
+pub struct Overlay;
357
+
358
+impl Overlay {
359
+ pub fn new() -> Self {
360
+ Self
361
+ }
362
+
363
+ /// Prostokąt dziecka nakładki o rozmiarze `size`, wyrównanego w `rect`.
364
+ pub fn child_rect(
365
+ &self,
366
+ rect: Rect,
367
+ size: Size,
368
+ halign: BinAlign,
369
+ valign: BinAlign,
370
+ margin: f64,
371
+ ) -> Rect {
372
+ let inner = rect.deflate(libluna_core::Insets::splat(margin));
373
+ let w = size.width.min(inner.width());
374
+ let h = size.height.min(inner.height());
375
+ let x = match halign {
376
+ BinAlign::Start => inner.x(),
377
+ BinAlign::Center => inner.x() + (inner.width() - w) / 2.0,
378
+ BinAlign::End => inner.right() - w,
379
+ };
380
+ let y = match valign {
381
+ BinAlign::Start => inner.y(),
382
+ BinAlign::Center => inner.y() + (inner.height() - h) / 2.0,
383
+ BinAlign::End => inner.bottom() - h,
384
+ };
385
+ Rect::from_xywh(x, y, w, h)
386
+ }
387
+}
388
+
389
+/// Ramka utrzymująca proporcje dziecka (szerokość/wysokość).
390
+#[derive(Debug, Clone, Copy)]
391
+pub struct AspectFrame {
392
+ ratio: f64,
393
+}
394
+
395
+impl AspectFrame {
396
+ pub fn new(ratio: f64) -> Self {
397
+ Self { ratio }
398
+ }
399
+
400
+ /// Prostokąt dziecka o zadanych proporcjach, wpisany w `rect` (wyśrodkowany).
401
+ pub fn content_rect(&self, rect: Rect) -> Rect {
402
+ if self.ratio <= 0.0 || rect.width() <= 0.0 || rect.height() <= 0.0 {
403
+ return rect;
404
+ }
405
+ let mut w = rect.width();
406
+ let mut h = w / self.ratio;
407
+ if h > rect.height() {
408
+ h = rect.height();
409
+ w = h * self.ratio;
410
+ }
411
+ Rect::from_xywh(
412
+ rect.x() + (rect.width() - w) / 2.0,
413
+ rect.y() + (rect.height() - h) / 2.0,
414
+ w,
415
+ h,
416
+ )
417
+ }
418
+}
419
+
420
+/// Pomocnik: ucinanie napisu z wielokropkiem do zadanej szerokości.
421
+pub fn ellipsize(text: &str, max_width: f64, size: f64, text_engine: &TextEngine) -> String {
422
+ if text_engine.measure(text, size, weight::NORMAL).width <= max_width {
423
+ return text.to_owned();
424
+ }
425
+ let mut out = String::new();
426
+ for ch in text.chars() {
427
+ let mut candidate = out.clone();
428
+ candidate.push(ch);
429
+ candidate.push('…');
430
+ if text_engine.measure(&candidate, size, weight::NORMAL).width > max_width {
431
+ break;
432
+ }
433
+ out.push(ch);
434
+ }
435
+ out.push('…');
436
+ out
437
+}
438
+
439
+#[cfg(test)]
440
+mod tests {
441
+ use super::*;
442
+
443
+ fn rect() -> Rect {
444
+ Rect::from_xywh(0.0, 0.0, 200.0, 40.0)
445
+ }
446
+
447
+ #[test]
448
+ fn progress_fraction_clamps() {
449
+ let mut p = ProgressBar::new().fraction(1.5);
450
+ assert_eq!(p.fraction_value(), 1.0);
451
+ p.set_fraction(-1.0);
452
+ assert_eq!(p.fraction_value(), 0.0);
453
+ }
454
+
455
+ #[test]
456
+ fn separator_line_is_centered() {
457
+ let s = Separator::horizontal();
458
+ assert_eq!(s.orientation, Orientation::Horizontal);
459
+ }
460
+
461
+ #[test]
462
+ fn revealer_clips_by_fraction() {
463
+ let mut r = Revealer::new();
464
+ r.set_fraction(0.5);
465
+ let rr = r.reveal_rect(rect());
466
+ assert_eq!(rr.height(), 20.0);
467
+ r.set_open(true);
468
+ assert_eq!(r.reveal_rect(rect()).height(), 40.0);
469
+ r.set_open(false);
470
+ assert_eq!(r.reveal_rect(rect()).height(), 0.0);
471
+ }
472
+
473
+ #[test]
474
+ fn aspect_frame_fits_ratio() {
475
+ let a = AspectFrame::new(2.0); // szersze niż wyższe
476
+ let r = a.content_rect(Rect::from_xywh(0.0, 0.0, 100.0, 100.0));
477
+ assert_eq!(r.width(), 100.0);
478
+ assert_eq!(r.height(), 50.0);
479
+ assert_eq!(r.y(), 25.0);
480
+ }
481
+
482
+ #[test]
483
+ fn overlay_anchors_child() {
484
+ let o = Overlay::new();
485
+ let r = o.child_rect(
486
+ rect(),
487
+ Size::new(20.0, 10.0),
488
+ BinAlign::End,
489
+ BinAlign::Start,
490
+ 4.0,
491
+ );
492
+ assert_eq!(r.right(), 196.0);
493
+ assert_eq!(r.y(), 4.0);
494
+ }
495
+
496
+ #[test]
497
+ fn ellipsize_shortens() {
498
+ let engine = TextEngine::system();
499
+ let out = ellipsize("bardzo długi napis do ucięcia", 20.0, 14.0, &engine);
500
+ assert!(out.ends_with('…'));
501
+ assert!(out.chars().count() < "bardzo długi napis do ucięcia".chars().count());
502
+ }
503
+}
@@ -43,6 +43,8 @@
43
43
//! - [`PreferencesGroup`] / [`PreferencesPage`] — grupa i strona ustawień.
44
44
//! - [`Dialog`] (+ [`AlertDialog`], [`AboutDialog`], [`ShortcutsDialog`], [`PreferencesDialog`]) — okna modalne.
45
45
//! - [`NavigationView`] / [`NavigationSplitView`] / [`OverlaySplitView`] / [`Sidebar`] — nawigacja.
46
+//! - [`Label`], [`Separator`], [`ProgressBar`], [`LevelBar`], [`Frame`], [`Revealer`], [`Overlay`],
47
+//! [`AspectFrame`] — widgety prezentacji (GTK4 Display).
46
48
//! - [`Carousel`] (alias [`Pager`]), [`ToolbarView`], [`WindowTitle`], [`Clamp`], [`WrapBox`],
47
49
//! [`BreakpointBin`], [`ButtonContent`] — pomocniki.
48
50
//!
@@ -66,6 +68,7 @@ pub mod card;
66
68
pub mod check_button;
67
69
pub mod ctx;
68
70
pub mod dialogs;
71
+pub mod display;
69
72
pub mod dropdown;
70
73
pub mod entry;
71
74
pub mod header_bar;
@@ -109,6 +112,10 @@ pub use ctx::PaintCtx;
109
112
pub use dialogs::{
110
113
AboutDialog, AlertDialog, AlertRects, Dialog, DialogRects, PreferencesDialog, ShortcutsDialog,
111
114
};
115
+pub use display::{
116
+ AspectFrame, Frame, Label, LabelAlign, LevelBar, Orientation, Overlay, ProgressBar, Revealer,
117
+ Separator,
118
+};
112
119
pub use dropdown::DropDown;
113
120
pub use entry::{Entry, EntryIcon, TextInput};
114
121
pub use header_bar::{ControlsLayout, ControlsSide, HeaderBar, TitleAlign, WindowControl};