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

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
+
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”).