← luna

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