← luna

Commit fe55be9

7
files
+297
added
-10
deleted
@@ -323,6 +323,8 @@ XDG_CONFIG_HOME="$PWD/examples" cargo run -p luna-demo --bin render -- --lang fr
323 323 wejścia** (wskaźnik, klawiatura, scroll) w `Frame`, odświeżanie na żądanie, zarządzanie
324 324 motywem przez `Frame::set_preference` / `set_accent` (zapis + natychmiastowe przerysowanie).
325 325 `Frame::close/minimize/toggle_maximize` to **realne akcje okna** (`xdg_toplevel`).
326 + **Klatki animacji**: aplikacja czyta `Frame::delta_time()` i woła `Frame::request_frame()`,
327 + dopóki trwa animacja — okno odświeża się wtedy płynnie (~60 Hz), a bez tego tylko na zdarzenia.
326 328 - `libluna-widgets`: **widgety wielokrotnego użycia** w modelu immediate — `ViewSwitcher`
327 329 (segmentowy), `NavList` (menu nawigacji z sekcjami), `Menu` (popover `☰`), `Button`
328 330 (z wariantami i chevronem), `Switch`, `Slider`, `DropDown` (z rozwinięciem przez `Menu`),
@@ -420,6 +422,11 @@ Nowe widgety — `Banner`, `Spinner`, `Tooltip` (`cargo run -p luna-demo --bin w
420 422
421 423 ![Banner, Spinner, Tooltip](docs/images/widgets.png)
422 424
425 +Pełna galeria nowych widgetów libLuny (cz. 5–12) — kontenery, tabele, kalendarz, chooser’y,
426 +kreator kroków, pasek stanu itd. (`cargo run -p luna-demo --bin gallery`):
427 +
428 +![Galeria widgetów](docs/images/gallery.png)
429 +
423 430 Przebieg interakcji (headless `simulate`) — m.in. menu `☰`, dolny panel, powiadomienie (`Toast`)
424 431 karta 12 i ekran stanu („Brak wyników”) klatka 14:
425 432
426 433 new file mode 100644
@@ -0,0 +1,230 @@
1 +//! Galeria nowych widgetów libLuny (cz. 5–12) — render headless.
2 +//!
3 +//! Uruchom: `cargo run -p luna-demo --bin gallery`
4 +//! Efekt: `docs/images/gallery.png`
5 +
6 +use std::error::Error;
7 +
8 +use libluna_core::style::weight;
9 +use libluna_core::{Color, Rect, Size, Theme};
10 +use libluna_render::{Scene, SoftwareRenderer, TextEngine};
11 +use libluna_widgets::{
12 + Adjustment, Assistant, Calendar, ColorButton, ColorChooser, Column, ColumnView, EmojiChooser,
13 + Expander, FlowBox, Image, ImageIcon, LinkButton, MenuButton, NavigationRail, Orientation,
14 + PaintCtx, Paned, PasswordEntry, Popover, PopoverPlacement, Scrollbar, SpinButton, Statusbar,
15 + ToggleButton,
16 +};
17 +
18 +const W: u32 = 960;
19 +const H: u32 = 760;
20 +
21 +/// Symbole rysowane przez font interfejsu (emoji kolorowe wymagają osobnego fontu).
22 +const SYMBOLS: [&str; 12] = ["★", "♥", "♪", "☀", "✓", "✗", "✚", "●", "◆", "■", "▲", "☰"];
23 +
24 +fn main() -> Result<(), Box<dyn Error>> {
25 + let theme = Theme::light();
26 + let text = TextEngine::system();
27 + let size = Size::new(W as f64, H as f64);
28 +
29 + let mut scene = Scene::new();
30 + let mut paint = PaintCtx {
31 + scene: &mut scene,
32 + theme: &theme,
33 + text: &text,
34 + };
35 + paint
36 + .scene
37 + .fill_rect(Rect::from_size(size), theme.colors.window_bg);
38 +
39 + // — Wiersz 1: rail, tabela, kalendarz —
40 + caption(
41 + &mut paint,
42 + 16.0,
43 + 14.0,
44 + "NavigationRail / ColumnView / Calendar",
45 + );
46 +
47 + let rail = NavigationRail::new(["Overview", "Network", "System", "About"]);
48 + rail.paint(&mut paint, Rect::from_xywh(16.0, 40.0, 180.0, 200.0));
49 +
50 + let mut table = ColumnView::new(vec![
51 + Column::new("Name").weight(2.0),
52 + Column::new("Size"),
53 + Column::new("Date"),
54 + ]);
55 + table.set_rows(5);
56 + table.set_selected(Some(1));
57 + let table_rect = Rect::from_xywh(216.0, 40.0, 420.0, 200.0);
58 + table.paint(&mut paint, table_rect);
59 + for (i, name) in [
60 + "Projects",
61 + "Report.pdf",
62 + "Notes.txt",
63 + "Budget.xlsx",
64 + "Backup",
65 + ]
66 + .iter()
67 + .enumerate()
68 + {
69 + let cell = table.cell_rect(table_rect, &theme, i, 0);
70 + paint.label_left(
71 + cell.x() + theme.spacing.md,
72 + cell.center().y,
73 + name,
74 + theme.typography.body.size,
75 + weight::NORMAL,
76 + theme.colors.text,
77 + );
78 + }
79 +
80 + let calendar = Calendar::new(2024, 1);
81 + let weekdays: Vec<String> = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
82 + .iter()
83 + .map(|s| s.to_string())
84 + .collect();
85 + calendar.paint(
86 + &mut paint,
87 + Rect::from_xywh(656.0, 40.0, 288.0, 200.0),
88 + "January 2024",
89 + &weekdays,
90 + );
91 +
92 + // — Wiersz 2: przyciski, pola, chooser’y —
93 + caption(&mut paint, 16.0, 256.0, "Buttons / Entries / Choosers");
94 +
95 + let toggle = ToggleButton::new("Bold").active(true);
96 + let toggle_rect = Rect::from_xywh(16.0, 282.0, 90.0, toggle.height(&theme));
97 + toggle.paint(&mut paint, toggle_rect);
98 +
99 + LinkButton::new("Documentation").paint(&mut paint, Rect::from_xywh(116.0, 282.0, 130.0, 34.0));
100 +
101 + ColorButton::new(Color::from_rgba8(0x91, 0x41, 0xac, 0xff))
102 + .paint(&mut paint, Rect::from_xywh(256.0, 286.0, 28.0, 26.0));
103 +
104 + MenuButton::burger().paint(&mut paint, Rect::from_xywh(296.0, 282.0, 40.0, 34.0));
105 + MenuButton::new("Options").paint(&mut paint, Rect::from_xywh(344.0, 282.0, 110.0, 34.0));
106 +
107 + SpinButton::new(65.0, 0.0, 100.0).paint(&mut paint, Rect::from_xywh(464.0, 282.0, 110.0, 34.0));
108 +
109 + let mut password = PasswordEntry::new("Password");
110 + password.insert_str("hunter2");
111 + password.paint(&mut paint, Rect::from_xywh(588.0, 282.0, 160.0, 34.0));
112 +
113 + for (i, icon) in [
114 + ImageIcon::Folder,
115 + ImageIcon::File,
116 + ImageIcon::Search,
117 + ImageIcon::Burger,
118 + ]
119 + .into_iter()
120 + .enumerate()
121 + {
122 + Image::new(icon).size(28.0).paint(
123 + &mut paint,
124 + Rect::from_xywh(760.0 + i as f64 * 36.0, 282.0, 28.0, 34.0),
125 + );
126 + }
127 +
128 + let mut chooser = ColorChooser::new(ColorChooser::default_palette());
129 + chooser.set_columns(4);
130 + chooser.paint(
131 + &mut paint,
132 + Rect::from_xywh(16.0, 332.0, 220.0, chooser.height(220.0, &theme)),
133 + );
134 +
135 + let symbols: Vec<String> = SYMBOLS.iter().map(|s| s.to_string()).collect();
136 + let mut emoji = EmojiChooser::new(symbols);
137 + emoji.set_columns(6);
138 + emoji.set_selected(1);
139 + emoji.paint(
140 + &mut paint,
141 + Rect::from_xywh(256.0, 332.0, 400.0, emoji.height(400.0, &theme)),
142 + );
143 +
144 + let mut flow = FlowBox::new(56.0);
145 + flow.set_count(8);
146 + flow.set_selected(Some(2));
147 + flow.paint(&mut paint, Rect::from_xywh(676.0, 332.0, 268.0, 120.0));
148 +
149 + // — Wiersz 3: expander, paned, assistant, scrollbar, popover —
150 + caption(
151 + &mut paint,
152 + 16.0,
153 + 486.0,
154 + "Expander / Paned / Assistant / Scrollbar / Popover",
155 + );
156 +
157 + let expander = Expander::new("Advanced options").expanded(true);
158 + let exp_rect = Rect::from_xywh(16.0, 512.0, 220.0, 120.0);
159 + expander.paint(&mut paint, exp_rect);
160 + let content = expander.content_rect(exp_rect, &theme);
161 + paint.label_left(
162 + content.x() + theme.spacing.md,
163 + content.y() + 16.0,
164 + "Extra settings…",
165 + theme.typography.caption.size,
166 + weight::NORMAL,
167 + theme.colors.dim_text,
168 + );
169 +
170 + let mut paned = Paned::new(Orientation::Horizontal);
171 + paned.set_position(0.4);
172 + let paned_rect = Rect::from_xywh(256.0, 512.0, 300.0, 120.0);
173 + let (a, b) = paned.panes(paned_rect, &theme);
174 + paint.scene.fill_rect(a, theme.colors.shade);
175 + paint.scene.fill_rect(b, theme.colors.card_bg);
176 + paned.paint(&mut paint, paned_rect);
177 +
178 + Assistant::new(["Welcome", "Details", "Done"]).paint(
179 + &mut paint,
180 + Rect::from_xywh(576.0, 512.0, 368.0, 120.0),
181 + "Cancel",
182 + "Next",
183 + );
184 +
185 + let mut bar = Scrollbar::new(Orientation::Horizontal);
186 + let mut adj = Adjustment::new(0.0, 200.0, 60.0);
187 + adj.set_fraction(0.35);
188 + bar.set_adjustment(adj);
189 + bar.paint(&mut paint, Rect::from_xywh(16.0, 648.0, 300.0, 24.0));
190 +
191 + let popover = Popover::new(PopoverPlacement::Above);
192 + let pop_rect = Rect::from_xywh(560.0, 640.0, 200.0, 56.0);
193 + popover.paint(&mut paint, pop_rect);
194 + paint.label_left(
195 + pop_rect.x() + theme.spacing.md,
196 + pop_rect.center().y,
197 + "Popover content",
198 + theme.typography.body.size,
199 + weight::NORMAL,
200 + theme.colors.text,
201 + );
202 +
203 + // — Pasek stanu na dole —
204 + let mut status = Statusbar::new();
205 + status.set_message("Ready");
206 + status.set_right("8 items");
207 + let sh = status.height(&theme);
208 + status.paint(
209 + &mut paint,
210 + Rect::from_xywh(0.0, size.height - sh, size.width, sh),
211 + );
212 +
213 + let mut renderer = SoftwareRenderer::new(W, H);
214 + renderer.draw(&scene, 1.0, theme.colors.window_bg);
215 + let png = renderer.encode_png().ok_or("failed to encode PNG")?;
216 + std::fs::write("docs/images/gallery.png", png)?;
217 + println!("saved docs/images/gallery.png ({W}x{H})");
218 + Ok(())
219 +}
220 +
221 +fn caption(paint: &mut PaintCtx, x: f64, y: f64, text: &str) {
222 + paint.label_left(
223 + x,
224 + y + 8.0,
225 + text,
226 + paint.theme.typography.caption.size,
227 + weight::BOLD,
228 + paint.theme.colors.dim_text,
229 + );
230 +}
@@ -66,6 +66,12 @@ fn main() -> Result<(), Box<dyn Error>> {
66 66 frame.set_style(settings.style().map(str::to_owned));
67 67 }
68 68
69 + // Animacja powiadomienia: krok i prośba o kolejną klatkę, dopóki trwa.
70 + if demo.toast.is_animating() {
71 + demo.toast.advance(frame.delta_time(), 0.3);
72 + frame.request_frame();
73 + }
74 +
69 75 // Motyw po zmianach (np. wybór stylu) — klatka od razu poprawna.
70 76 let theme = settings.resolve_themed(frame.system_dark());
71 77 ui::build_scene(
@@ -678,12 +678,12 @@ pub fn handle_events(
678 678 if demo.toast.is_open() {
679 679 if demo.toast.action_at(size, theme, text, p) {
680 680 settings.set_accent(demo.prev_accent);
681 - demo.toast.set_open(false);
681 + demo.toast.set_open_animated(false);
682 682 continue;
683 683 }
684 684 // Klik w treść powiadomienia je zamyka (akcent zostaje).
685 685 if demo.toast.dismiss_at(size, theme, text, p) {
686 - demo.toast.set_open(false);
686 + demo.toast.set_open_animated(false);
687 687 continue;
688 688 }
689 689 }
@@ -968,7 +968,7 @@ fn apply_accent(settings: &mut ThemeSettings, demo: &mut Demo, accent: Accent) {
968 968 demo.prev_accent = settings.accent();
969 969 settings.set_accent(accent);
970 970 demo.toast = Toast::new(tr("Accent changed")).action(tr("Undo"));
971 - demo.toast.show();
971 + demo.toast.show_animated();
972 972 }
973 973
974 974 fn cycle_language() {
@@ -1062,8 +1062,8 @@ pub fn build_scene(
1062 1062 draw_sheet_content(&mut paint, demo, size);
1063 1063 }
1064 1064
1065 - // Powiadomienie („toast”) nad wszystkim.
1066 - if demo.toast.is_open() {
1065 + // Powiadomienie („toast”) nad wszystkim (także w trakcie wygaszania).
1066 + if demo.toast.progress() > 0.001 {
1067 1067 demo.toast.paint(&mut paint, size);
1068 1068 }
1069 1069 }
@@ -9,7 +9,7 @@
9 9 //! Dekoracje są po stronie klienta (CSD) — rysujemy własny HeaderBar.
10 10
11 11 use std::error::Error;
12 -use std::time::Duration;
12 +use std::time::{Duration, Instant};
13 13
14 14 use libluna_core::{
15 15 Accent, ButtonState, Event, KeyEvent, Modifiers, Point, PointerButton, PointerEvent,
@@ -117,8 +117,11 @@ pub struct Frame<'a> {
117 117 pub scale: f64,
118 118 /// Zdarzenia wejścia od poprzedniej klatki.
119 119 pub events: &'a [Event],
120 + /// Czas od poprzedniej klatki (sekundy) — dla animacji (`advance(dt, …)`).
121 + pub dt: f64,
120 122 settings: &'a mut ThemeSettings,
121 123 actions: &'a mut WindowActions,
124 + frame_requested: &'a mut bool,
122 125 system_dark: bool,
123 126 }
124 127
@@ -137,6 +140,17 @@ impl Frame<'_> {
137 140 pub fn toggle_maximize(&mut self) {
138 141 self.actions.toggle_maximize = true;
139 142 }
143 +
144 + /// Czas od poprzedniej klatki w sekundach (do kroku animacji).
145 + pub fn delta_time(&self) -> f64 {
146 + self.dt
147 + }
148 +
149 + /// Prosi o kolejną klatkę — używane, gdy trwa animacja (utrzymuje płynność).
150 + /// Dopóki aplikacja nie zawoła tej metody, okno odświeża się tylko na zdarzenia.
151 + pub fn request_frame(&mut self) {
152 + *self.frame_requested = true;
153 + }
140 154 /// Bieżące ustawienia wyglądu (kopia).
141 155 pub fn settings(&self) -> ThemeSettings {
142 156 self.settings.clone()
@@ -244,11 +258,19 @@ where
244 258 pending_events: Vec::new(),
245 259 needs_redraw: true,
246 260 actions: WindowActions::default(),
261 + frame_requested: false,
262 + last_tick: Instant::now(),
247 263 maximized: false,
248 264 };
249 265
250 266 while !app.exit {
251 - event_loop.dispatch(Duration::from_millis(100), &mut app)?;
267 + // Gdy trwa animacja (aplikacja prosi o klatki), odświeżamy płynnie (~60 Hz).
268 + let timeout = if app.needs_redraw {
269 + Duration::from_millis(16)
270 + } else {
271 + Duration::from_millis(100)
272 + };
273 + event_loop.dispatch(timeout, &mut app)?;
252 274 if app.needs_redraw {
253 275 app.redraw();
254 276 }
@@ -281,6 +303,10 @@ struct App {
281 303 pending_events: Vec<Event>,
282 304 needs_redraw: bool,
283 305 actions: WindowActions,
306 + /// Aplikacja poprosiła o kolejną klatkę (animacja w toku).
307 + frame_requested: bool,
308 + /// Znacznik czasu poprzedniej klatki (do liczenia `dt`).
309 + last_tick: Instant,
284 310 maximized: bool,
285 311 }
286 312
@@ -291,20 +317,25 @@ impl App {
291 317 }
292 318
293 319 fn redraw(&mut self) {
320 + let now = Instant::now();
321 + let dt = (now - self.last_tick).as_secs_f64().clamp(0.0, 0.1);
322 + self.last_tick = now;
323 +
294 324 let events = std::mem::take(&mut self.pending_events);
295 325 self.needs_redraw = false;
296 326 self.actions = WindowActions::default();
327 + self.frame_requested = false;
297 328
298 329 self.settings.clear_dirty();
299 330 let mut theme = self.settings.resolve(self.system_dark);
300 - self.render_frame(theme, &events);
331 + self.render_frame(theme, &events, dt);
301 332
302 333 // Jeśli domknięcie zmieniło motyw w trakcie, przerysuj bez zdarzeń.
303 334 let mut guard = 0;
304 335 while self.settings.is_dirty() && guard < 4 {
305 336 self.settings.clear_dirty();
306 337 theme = self.settings.resolve(self.system_dark);
307 - self.render_frame(theme, &[]);
338 + self.render_frame(theme, &[], dt);
308 339 guard += 1;
309 340 }
310 341 if self.settings.is_dirty() {
@@ -314,6 +345,11 @@ impl App {
314 345
315 346 self.present();
316 347 self.apply_actions();
348 +
349 + // Aplikacja poprosiła o kolejną klatkę (animacja) — odśwież ponownie.
350 + if self.frame_requested {
351 + self.needs_redraw = true;
352 + }
317 353 }
318 354
319 355 /// Wykonuje akcje okna zgłoszone w trakcie klatki.
@@ -334,7 +370,7 @@ impl App {
334 370 }
335 371 }
336 372
337 - fn render_frame(&mut self, theme: Theme, events: &[Event]) {
373 + fn render_frame(&mut self, theme: Theme, events: &[Event], dt: f64) {
338 374 let (width, height, scale, system_dark) =
339 375 (self.width, self.height, self.scale, self.system_dark);
340 376 let phys_w = (width as f64 * scale).round().max(1.0) as u32;
@@ -348,6 +384,7 @@ impl App {
348 384 renderer,
349 385 settings,
350 386 actions,
387 + frame_requested,
351 388 ..
352 389 } = self;
353 390 let mut frame = Frame {
@@ -357,8 +394,10 @@ impl App {
357 394 size: Size::new(width as f64, height as f64),
358 395 scale,
359 396 events,
397 + dt,
360 398 settings,
361 399 actions,
400 + frame_requested,
362 401 system_dark,
363 402 };
364 403 render(&mut frame);
365 404 new file mode 100644
366 405 Binary files /dev/null and b/docs/images/gallery.png differ
@@ -63,8 +63,13 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
63 63 (`Easing`, `lerp`, `Animation`), użyte w `Stack` (przejścia `StackTransition`), `Revealer`
64 64 i `Toast` (sterowane `advance(dt, …)`).
65 65 - `libluna-platform`: realne akcje okna z `Frame` (`close`/`minimize`/`toggle_maximize`).
66 + **Klatki animacji**: `Frame::delta_time()` + `Frame::request_frame()` (płynne ~60 Hz, gdy trwa
67 + animacja; bez tego okno odświeża się tylko na zdarzenia).
66 68 - Demo używa widgetów zamiast ręcznego rysowania (pasek boczny, wybór motywu/widoku, menu `☰`,
67 69 przycisk „Opcje”, przełączniki, suwak, wybór języka, pasek tytułu, zakładki `TabBar`/`TabView`).
70 + Powiadomienie (`Toast`) w demo animuje się (wjazd + fade).
71 +- **Galeria widgetów** headless: `cargo run -p luna-demo --bin gallery` → `docs/images/gallery.png`
72 + (kontenery, tabela, kalendarz, chooser’y, kreator kroków, pasek stanu z cz. 5–12).
68 73 - Motywy: wariant (light/dark/auto) + akcent, z trwałością. **Component metrics**: rozmiary
69 74 i paddingi kontrolek wyniesione z widgetów do tokenów (`Theme::metrics`), więc styl steruje
70 75 też geometrią (README, „Własny wygląd”).