Commit f69143f
15
files
+161
added
-32
deleted
@@ -194,7 +194,8 @@ cargo run -p libluna --example minimal
194
194
(ikona zmienia się w `▭`), **`☰`** aby otworzyć menu (zamyka się klikiem poza panelem),
195
195
lub kontrolkę **języka** (`DropDown`). Przycisk **„Opcje ▾”** otwiera **popover opcji**
196
196
(`Stepper` rozmiaru ikon, `RadioGroup` sortowania, `CheckButton` ukrytych plików).
197
- Z menu **`☰` → „O aplikacji”** otwiera się **dolny panel**
197
+ Z menu **`☰` → „Styl”** wybierzesz **styl z pliku** (natychmiast stosowany i zapisywany),
198
+ a **`☰` → „O aplikacji”** otwiera **dolny panel**
198
199
(`BottomSheet` z karuzelą stron `Pager` i polem tekstowym `Entry` — klik = fokus, wpisywanie),
199
200
zamykany klikiem w tło, przyciskiem `✕` lub
200
201
przeciągnięciem w dół. W karcie **Pliki** kliknij element **okruszków** (nawigacja „w górę”),
@@ -268,9 +269,10 @@ cargo run -p luna-demo --bin render -- --theme examples/luna/themes/nord.theme d
268
269
cargo run -p luna-demo --bin layout -- --theme examples/luna/themes/nord.theme --out docs/images/layout-nord.png
269
270
```
270
271
271
-Do zrobienia pozostaje **interaktywny wybór stylu w Ustawieniach** oraz **okno podglądu**
272
-na żywo przy wyborze — silnik (skanowanie katalogów: `available_themes()`, wczytywanie,
273
-nakładanie) jest już gotowy.
272
+Do zrobienia pozostaje **okno podglądu na żywo** przy wyborze (miniatura). Sam wybór stylu
273
+działa już w demo: **menu `☰` → „Styl”** otwiera listę („Domyślny” + style z plików),
274
+a wybór jest natychmiast stosowany i zapisywany w `theme.conf`. W headless demo/testach katalog
275
+ze stylami wskazuje `LUNA_THEMES_DIR`.
274
276
275
277
### Co jeszcze decyduje o „looku GTK4”
276
278
@@ -31,7 +31,7 @@ fn press(p: Point) -> Event {
31
31
}
32
32
33
33
fn render_pixels(settings: &ThemeSettings, demo: &Demo, text: &TextEngine, size: Size) -> Vec<u8> {
34
- let theme = settings.resolve(false);
34
+ let theme = settings.resolve_themed(false);
35
35
let mut scene = Scene::new();
36
36
ui::build_scene(
37
37
&mut scene,
@@ -55,6 +55,10 @@ fn save_png(path: &str, pixels: &[u8]) -> Result<(), Box<dyn Error>> {
55
55
56
56
fn main() -> Result<(), Box<dyn Error>> {
57
57
set_locale(Locale::new("pl"));
58
+ // Katalog ze stylami (przykłady w repozytorium) — dla kroku wyboru stylu.
59
+ let themes =
60
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/luna/themes");
61
+ std::env::set_var("LUNA_THEMES_DIR", themes);
58
62
59
63
let size = Size::new(W as f64, H as f64);
60
64
let text = TextEngine::system();
@@ -71,7 +75,7 @@ fn main() -> Result<(), Box<dyn Error>> {
71
75
// Pomocniczo: aktualny layout i wysłanie kliknięcia.
72
76
macro_rules! layout {
73
77
() => {
74
- ui::layout(size, &settings.resolve(false), &demo)
78
+ ui::layout(size, &settings.resolve_themed(false), &demo)
75
79
};
76
80
}
77
81
macro_rules! click {
@@ -176,7 +180,7 @@ fn main() -> Result<(), Box<dyn Error>> {
176
180
// 9. Klik „O aplikacji” w otwartym menu → dolny panel (BottomSheet + Pager).
177
181
let theme = settings.resolve(false);
178
182
let about = ui::popover_menu(size, &theme, &text, &layout!(), &demo)
179
- .map(|(panel, menu)| menu.rows(panel, &theme)[4].rect.center())
183
+ .map(|(panel, menu)| menu.rows(panel, &theme)[5].rect.center())
180
184
.expect("menu is open");
181
185
click!(about);
182
186
assert!(demo.sheet.is_open(), "bottom sheet did not open");
@@ -296,6 +300,29 @@ fn main() -> Result<(), Box<dyn Error>> {
296
300
frames.push(render_pixels(&settings, &demo, &text, size));
297
301
labels.push("15. Filter cleared".into());
298
302
303
+ // 16. Wybór stylu: ☰ → Styl → Nord (styl z pliku, steruje kolorem i geometrią).
304
+ let c = layout!().burger.center();
305
+ click!(c);
306
+ assert_eq!(demo.popover, Some(Popover::Menu), "menu did not open");
307
+ let theme = settings.resolve_themed(false);
308
+ let style_item = ui::popover_menu(size, &theme, &text, &layout!(), &demo)
309
+ .map(|(panel, menu)| menu.rows(panel, &theme)[4].rect.center())
310
+ .expect("style entry");
311
+ click!(style_item);
312
+ assert_eq!(
313
+ demo.popover,
314
+ Some(Popover::Styles),
315
+ "styles popover did not open"
316
+ );
317
+ let theme = settings.resolve_themed(false);
318
+ let nord = ui::popover_menu(size, &theme, &text, &layout!(), &demo)
319
+ .map(|(panel, menu)| menu.rows(panel, &theme)[1].rect.center())
320
+ .expect("style list");
321
+ click!(nord);
322
+ assert_eq!(settings.style(), Some("nord"), "style not applied");
323
+ frames.push(render_pixels(&settings, &demo, &text, size));
324
+ labels.push("16. Style: Nord".into());
325
+
299
326
// Zapis klatek.
300
327
for (i, pixels) in frames.iter().enumerate() {
301
328
let path = format!("docs/images/demo-step-{}.png", i + 1);
@@ -15,8 +15,6 @@ fn main() -> Result<(), Box<dyn Error>> {
15
15
16
16
// Ustawienia wyglądu wczytane z dysku (jeśli są).
17
17
let settings = ThemeSettings::load();
18
- // Wybrany styl (instalowany z pliku) — wczytany raz, nakładany na każdą klatkę.
19
- let style_patch = settings.style().and_then(libluna_core::theme_file::load);
20
18
// Tryb systemowy: prosty przełącznik przez zmienną środowiskową (docelowo portal).
21
19
let system_dark = std::env::var("LUNA_SYSTEM_DARK")
22
20
.map(|v| !v.is_empty() && v != "0")
@@ -25,20 +23,16 @@ fn main() -> Result<(), Box<dyn Error>> {
25
23
let config = AppConfig::new("Aplikacja")
26
24
.app_id("eu.paganlinux.Luna.Settings")
27
25
.initial_size(900, 1028)
28
- .settings(settings.clone())
26
+ .settings(settings)
29
27
.system_dark(system_dark);
30
28
31
29
let mut demo = Demo::default();
32
30
33
31
run(config, move |frame: &mut Frame| {
34
- // Motyw rozwiązujemy z aktualnych ustawień (+ styl z pliku), aby klatka była poprawna.
35
- let mut theme = settings.resolve(frame.system_dark());
36
- if let Some(patch) = &style_patch {
37
- theme = patch.build(theme);
38
- }
39
-
40
- // Logika interakcji jest niezależna od Waylanda: dostaje zdarzenia i ustawienia.
41
32
let mut settings = frame.settings();
33
+
34
+ // Motyw (z ewentualnym stylem z pliku) do obsługi zdarzeń.
35
+ let theme = settings.resolve_themed(frame.system_dark());
42
36
ui::handle_events(
43
37
&mut settings,
44
38
frame.events,
@@ -47,6 +41,7 @@ fn main() -> Result<(), Box<dyn Error>> {
47
41
frame.text,
48
42
&mut demo,
49
43
);
44
+
50
45
// Akcje okna zgłoszone przez HeaderBar (kontrolki).
51
46
if demo.pending_close {
52
47
frame.close();
@@ -67,8 +62,12 @@ fn main() -> Result<(), Box<dyn Error>> {
67
62
if settings.accent() != frame.accent() {
68
63
frame.set_accent(settings.accent());
69
64
}
65
+ if settings.style() != frame.style() {
66
+ frame.set_style(settings.style().map(str::to_owned));
67
+ }
70
68
71
- // Motyw rozwiązujemy z aktualnych ustawień, aby klatka była już poprawna.
69
+ // Motyw po zmianach (np. wybór stylu) — klatka od razu poprawna.
70
+ let theme = settings.resolve_themed(frame.system_dark());
72
71
ui::build_scene(
73
72
frame.scene,
74
73
&theme,
@@ -22,7 +22,9 @@ const PAD: f64 = 12.0;
22
22
/// Wysokość wiersza ustawień w demo.
23
23
const ROW_H: f64 = 56.0;
24
24
/// Indeks pozycji „O aplikacji” w menu „☰” (otwiera dolny panel).
25
-const ABOUT_ENTRY: usize = 4;
25
+const ABOUT_ENTRY: usize = 5;
26
+/// Indeks pozycji „Styl” w menu „☰” (otwiera wybór stylu).
27
+const STYLE_ENTRY: usize = 4;
26
28
27
29
/// Domyślna szerokość paska bocznego.
28
30
pub const DEFAULT_SIDEBAR_W: f64 = 220.0;
@@ -41,6 +43,8 @@ pub enum Popover {
41
43
Language,
42
44
/// Popover opcji widoku pod przyciskiem „Opcje ▾”.
43
45
Options,
46
+ /// Lista wyboru stylu (instalowanego z pliku).
47
+ Styles,
44
48
}
45
49
46
50
/// Stan aplikacji demo, który nie należy do warstwy platformy ani do widgetów.
@@ -227,10 +231,16 @@ pub fn menu_entries() -> Menu {
227
231
MenuEntry::action(tr("Keyboard Shortcuts")),
228
232
MenuEntry::separator(),
229
233
MenuEntry::action(tr("Settings")),
234
+ MenuEntry::action(tr("Style")),
230
235
MenuEntry::action(tr("About")),
231
236
])
232
237
}
233
238
239
+/// Lista dostępnych stylów (z plików) do popoveru wyboru.
240
+pub fn style_list() -> Vec<libluna_core::StyledTheme> {
241
+ libluna_core::available_themes()
242
+}
243
+
234
244
/// Pasek zakładek demo z przetłumaczonymi etykietami (tytuły w `TabView` to klucze).
235
245
pub fn tab_bar(demo: &Demo) -> TabBar {
236
246
let tabs: Vec<Tab> = demo
@@ -367,6 +377,16 @@ pub fn popover_menu(
367
377
}
368
378
// Popover „Opcje” rysowany osobno (własny układ).
369
379
Popover::Options => None,
380
+ // Wybór stylu: „Domyślny” + style z plików.
381
+ Popover::Styles => {
382
+ let mut entries = vec![MenuEntry::action(tr("Default"))];
383
+ for style in style_list() {
384
+ entries.push(MenuEntry::action(style.name));
385
+ }
386
+ let menu = Menu::new(entries);
387
+ let w = menu.width(text, theme);
388
+ Some((menu.place(lay.burger, w, viewport, theme), menu))
389
+ }
370
390
}
371
391
}
372
392
@@ -684,14 +704,33 @@ pub fn handle_events(
684
704
if demo.popover.is_some() {
685
705
if let (Some((panel, menu)), Some(kind)) = (&popover, demo.popover) {
686
706
if let Some(entry) = menu.index_at(*panel, p, theme) {
687
- if kind == Popover::Language {
688
- let codes = libluna_i18n::languages();
689
- if let Some(code) = codes.get(entry) {
690
- libluna_i18n::set_locale(Locale::new(code));
707
+ match kind {
708
+ Popover::Language => {
709
+ let codes = libluna_i18n::languages();
710
+ if let Some(code) = codes.get(entry) {
711
+ libluna_i18n::set_locale(Locale::new(code));
712
+ }
713
+ }
714
+ Popover::Menu if entry == STYLE_ENTRY => {
715
+ // „Styl” otwiera wybór stylu.
716
+ demo.popover = Some(Popover::Styles);
717
+ demo.popover_hover = None;
718
+ continue;
719
+ }
720
+ Popover::Menu if entry == ABOUT_ENTRY => {
721
+ // „O aplikacji” otwiera dolny panel.
722
+ demo.sheet.set_open(true);
723
+ }
724
+ Popover::Styles => {
725
+ // 0 = „Domyślny”, dalej style z plików.
726
+ if entry == 0 {
727
+ settings.set_style(None);
728
+ } else if let Some(style) = style_list().get(entry - 1)
729
+ {
730
+ settings.set_style(Some(style.id.clone()));
731
+ }
691
732
}
692
- } else if entry == ABOUT_ENTRY {
693
- // „O aplikacji” otwiera dolny panel.
694
- demo.sheet.set_open(true);
733
+ _ => {}
695
734
}
696
735
}
697
736
}
@@ -24,13 +24,13 @@ fn press(p: Point) -> Event {
24
24
25
25
/// Wysyła jedno kliknięcie do logiki demo.
26
26
fn click(settings: &mut ThemeSettings, demo: &mut Demo, p: Point) {
27
- let theme = settings.resolve(false);
27
+ let theme = settings.resolve_themed(false);
28
28
let text = TextEngine::system();
29
29
ui::handle_events(settings, &[press(p)], size(), &theme, &text, demo);
30
30
}
31
31
32
32
fn layout(settings: &ThemeSettings, demo: &Demo) -> ui::Layout {
33
- ui::layout(size(), &settings.resolve(false), demo)
33
+ ui::layout(size(), &settings.resolve_themed(false), demo)
34
34
}
35
35
36
36
#[test]
@@ -100,7 +100,7 @@ fn about_menu_opens_bottom_sheet_and_scrim_closes_it() {
100
100
let theme = settings.resolve(false);
101
101
let text = TextEngine::system();
102
102
let about = ui::popover_menu(size(), &theme, &text, &layout(&settings, &demo), &demo)
103
- .map(|(panel, menu)| menu.rows(panel, &theme)[4].rect.center())
103
+ .map(|(panel, menu)| menu.rows(panel, &theme)[5].rect.center())
104
104
.unwrap();
105
105
click(&mut settings, &mut demo, about);
106
106
assert!(demo.sheet.is_open());
@@ -387,7 +387,7 @@ fn sheet_entry_focus_and_typing() {
387
387
let theme = settings.resolve(false);
388
388
let text = TextEngine::system();
389
389
let about = ui::popover_menu(size(), &theme, &text, &layout(&settings, &demo), &demo)
390
- .map(|(panel, menu)| menu.rows(panel, &theme)[4].rect.center())
390
+ .map(|(panel, menu)| menu.rows(panel, &theme)[5].rect.center())
391
391
.expect("about entry");
392
392
click(&mut settings, &mut demo, about);
393
393
assert!(demo.sheet.is_open());
@@ -430,3 +430,34 @@ fn clicking_toast_body_dismisses_without_reverting() {
430
430
assert!(!demo.toast.is_open());
431
431
assert_eq!(settings.accent(), accent_after);
432
432
}
433
+
434
+#[test]
435
+fn choosing_style_from_menu_sets_style() {
436
+ // Wskaż katalog ze stylami z repozytorium (przykłady).
437
+ let themes =
438
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/luna/themes");
439
+ std::env::set_var("LUNA_THEMES_DIR", themes);
440
+
441
+ let mut settings = ThemeSettings::default();
442
+ let mut demo = Demo::default();
443
+ let theme = settings.resolve_themed(false);
444
+ let text = TextEngine::system();
445
+
446
+ // ☰ → „Styl” otwiera listę stylów.
447
+ let burger = layout(&settings, &demo).burger.center();
448
+ click(&mut settings, &mut demo, burger);
449
+ assert_eq!(demo.popover, Some(Popover::Menu));
450
+ let style_item = ui::popover_menu(size(), &theme, &text, &layout(&settings, &demo), &demo)
451
+ .map(|(panel, menu)| menu.rows(panel, &theme)[4].rect.center())
452
+ .expect("style entry");
453
+ click(&mut settings, &mut demo, style_item);
454
+ assert_eq!(demo.popover, Some(Popover::Styles));
455
+
456
+ // Wybór pierwszego stylu (Nord) ustawia go i zamyka popover.
457
+ let nord = ui::popover_menu(size(), &theme, &text, &layout(&settings, &demo), &demo)
458
+ .map(|(panel, menu)| menu.rows(panel, &theme)[1].rect.center())
459
+ .expect("style list");
460
+ click(&mut settings, &mut demo, nord);
461
+ assert_eq!(settings.style(), Some("nord"));
462
+ assert!(demo.popover.is_none());
463
+}
@@ -170,8 +170,18 @@ pub struct StyledTheme {
170
170
}
171
171
172
172
/// Katalogi wyszukiwania stylów (kolejność = priorytet).
173
+///
174
+/// Oprócz katalogów standardowych uwzględnia `LUNA_THEMES_DIR` — listę katalogów
175
+/// rozdzielonych `:` (przydatne w dev/testach, by wskazać katalog ze stylami).
173
176
pub fn search_dirs() -> Vec<PathBuf> {
174
177
let mut dirs = Vec::new();
178
+ if let Some(extra) = std::env::var_os("LUNA_THEMES_DIR") {
179
+ for part in std::env::split_paths(&extra) {
180
+ if !part.as_os_str().is_empty() {
181
+ dirs.push(part);
182
+ }
183
+ }
184
+ }
175
185
if let Some(home) = std::env::var_os("HOME") {
176
186
let home = PathBuf::from(home);
177
187
dirs.push(home.join(".config").join("luna").join("themes"));
@@ -52,6 +52,8 @@ Date modified = Änderungsdatum
52
52
Size = Größe
53
53
Show hidden files = Versteckte Dateien anzeigen
54
54
Your name = Dein Name
55
+Style = Stil
56
+Default = Standard
55
57
Accent changed = Akzent geändert
56
58
Undo = Rückgängig
57
59
No results = Keine Ergebnisse
@@ -53,6 +53,8 @@ Date modified = Date modified
53
53
Size = Size
54
54
Show hidden files = Show hidden files
55
55
Your name = Your name
56
+Style = Style
57
+Default = Default
56
58
Accent changed = Accent changed
57
59
Undo = Undo
58
60
No results = No results
@@ -52,6 +52,8 @@ Date modified = Daty modyfikacji
52
52
Size = Rozmiar
53
53
Show hidden files = Pokazuj ukryte pliki
54
54
Your name = Imię
55
+Style = Styl
56
+Default = Domyślny
55
57
Accent changed = Zmieniono akcent
56
58
Undo = Cofnij
57
59
No results = Brak wyników
@@ -169,6 +169,17 @@ impl Frame<'_> {
169
169
let _ = self.settings.save();
170
170
}
171
171
172
+ /// Nazwa wybranego stylu (instalowanego z pliku), jeśli ustawiona.
173
+ pub fn style(&self) -> Option<&str> {
174
+ self.settings.style()
175
+ }
176
+
177
+ /// Ustawia styl (instalowany z pliku); zapisuje i przerysowuje.
178
+ pub fn set_style(&mut self, style: Option<String>) {
179
+ self.settings.set_style(style);
180
+ let _ = self.settings.save();
181
+ }
182
+
172
183
/// Czy system zgłasza tryb ciemny.
173
184
pub const fn system_dark(&self) -> bool {
174
185
self.system_dark
175
186
Binary files a/docs/images/demo-montage.png and b/docs/images/demo-montage.png differ
176
187
new file mode 100644
177
188
Binary files /dev/null and b/docs/images/demo-step-16.png differ
178
189
Binary files a/docs/images/demo-step-8.png and b/docs/images/demo-step-8.png differ
@@ -55,8 +55,10 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
55
55
- **Style instalowane z plików**: `libluna-core::theme_file` (`ThemePatch`, klucze kropkowe,
56
56
`available_themes()`, nakładanie na bazę), trwały wybór stylu w `ThemeSettings` (`theme.conf`),
57
57
przykłady `examples/luna/themes/*.theme`, podgląd przez `render/layout --theme`.
58
-- Dalej: **interaktywny wybór stylu** w Ustawieniach + **okno podglądu** przy wyborze,
59
- a potem scroll/overflow w `TabBar`, migracja `luna-demo` na drzewo układu.
58
+- Wybór stylu w UI: menu `☰` → „Styl” (lista `available_themes()`, „Domyślny” + style z plików),
59
+ stosowany na żywo i zapisywany w `theme.conf`; `LUNA_THEMES_DIR` dla dev/testów.
60
+- Dalej: **okno podglądu** przy wyborze (miniatura z realnych widgetów), scroll/overflow
61
+ w `TabBar`, migracja `luna-demo` na drzewo układu.
60
62
61
63
## M2 — Układ + widgety bazowe
62
64
- Dwuprzebiegowy silnik measure/allocate — ✅ **częściowo** (silnik + kontenery `VBox`/`HBox`,
@@ -51,6 +51,8 @@ Date modified = Date de modification
51
51
Size = Taille
52
52
Show hidden files = Afficher les fichiers cachés
53
53
Your name = Votre nom
54
+Style = Style
55
+Default = Par défaut
54
56
Accent changed = Accent modifié
55
57
Undo = Annuler
56
58
No results = Aucun résultat