← luna

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};