← luna

Commit 816be05

7
files
+262
added
-33
deleted
@@ -302,6 +302,7 @@ version = "0.0.1"
302 302 dependencies = [
303 303 "libluna-core",
304 304 "libluna-i18n",
305 + "libluna-platform",
305 306 "libluna-render",
306 307 "libluna-widgets",
307 308 ]
@@ -7,8 +7,12 @@
7 7 //! wejścia (`seat`, `xkbcommon`) oraz pętlę zdarzeń.
8 8 //!
9 9 //! Punkt wejścia to [`run`] z [`AppConfig`]: otwiera okno i wywołuje domknięcie
10 -//! rysujące przy każdym odświeżeniu.
10 +//! rysujące przy każdym odświeżeniu. Gdy [`AppConfig::layer`] jest ustawione, zamiast
11 +//! okna powstaje **powierzchnia warstwowa** (`wlr-layer-shell`) — tak działa panel Luny.
11 12
12 13 pub mod window;
13 14
14 -pub use window::{run, AppConfig, Frame};
15 +pub use window::{run, AppConfig, Frame, LayerConfig};
16 +
17 +/// Warstwy powierzchni (`wlr-layer-shell`) — reeksport dla wygody odbiorców.
18 +pub use smithay_client_toolkit::shell::wlr_layer::{Anchor, Layer};
@@ -21,8 +21,8 @@ use smithay_client_toolkit::reexports::calloop::{EventLoop, LoopHandle};
21 21 use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
22 22 use smithay_client_toolkit::{
23 23 compositor::{CompositorHandler, CompositorState},
24 - delegate_compositor, delegate_keyboard, delegate_output, delegate_pointer, delegate_registry,
25 - delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window,
24 + delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer,
25 + delegate_registry, delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window,
26 26 output::{OutputHandler, OutputState},
27 27 registry::{ProvidesRegistryState, RegistryState},
28 28 registry_handlers,
@@ -32,6 +32,10 @@ use smithay_client_toolkit::{
32 32 Capability, SeatHandler, SeatState,
33 33 },
34 34 shell::{
35 + wlr_layer::{
36 + Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
37 + LayerSurfaceConfigure,
38 + },
35 39 xdg::{
36 40 window::{Window, WindowConfigure, WindowDecorations, WindowHandler},
37 41 XdgShell,
@@ -61,6 +65,56 @@ pub struct AppConfig {
61 65 pub settings: ThemeSettings,
62 66 /// Czy system zgłasza tryb ciemny (dla preferencji `System`).
63 67 pub system_dark: bool,
68 + /// Gdy ustawione — zamiast okna `xdg-shell` tworzymy powierzchnię `wlr-layer-shell`
69 + /// (panel/dock/tło). To tryb używany przez powłokę Luna.
70 + pub layer: Option<LayerConfig>,
71 +}
72 +
73 +/// Konfiguracja powierzchni warstwowej (`wlr-layer-shell`).
74 +#[derive(Debug, Clone)]
75 +pub struct LayerConfig {
76 + /// Przestrzeń nazw (np. „luna-panel”).
77 + pub namespace: String,
78 + /// Warstwa rysowania.
79 + pub layer: Layer,
80 + /// Kotwice krawędzi (np. `TOP|LEFT|RIGHT` = pasek na całą szerokość).
81 + pub anchor: Anchor,
82 + /// Rozmiar (0 = wybierze kompozytor/klient).
83 + pub size: (u32, u32),
84 + /// Marginesy: góra, prawa, dół, lewa.
85 + pub margin: (i32, i32, i32, i32),
86 + /// Strefa wyłączna (zarezerwowane miejsce; 0 = brak).
87 + pub exclusive_zone: i32,
88 + /// Czy panel przyjmuje fokus klawiatury na żądanie.
89 + pub keyboard: bool,
90 +}
91 +
92 +impl LayerConfig {
93 + /// Pasek na górnej krawędzi, na całą szerokość, o zadanej grubości.
94 + pub fn top_panel(namespace: impl Into<String>, thickness: u32, exclusive: bool) -> Self {
95 + Self {
96 + namespace: namespace.into(),
97 + layer: Layer::Top,
98 + anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
99 + size: (0, thickness),
100 + margin: (0, 0, 0, 0),
101 + exclusive_zone: if exclusive { thickness as i32 } else { 0 },
102 + keyboard: true,
103 + }
104 + }
105 +
106 + /// Panel nakładkowy (OSD/notyfikacje) na środku, bez strefy wyłącznej.
107 + pub fn overlay(namespace: impl Into<String>, width: u32, height: u32) -> Self {
108 + Self {
109 + namespace: namespace.into(),
110 + layer: Layer::Overlay,
111 + anchor: Anchor::TOP,
112 + size: (width, height),
113 + margin: (24, 0, 0, 0),
114 + exclusive_zone: 0,
115 + keyboard: false,
116 + }
117 + }
64 118 }
65 119
66 120 impl AppConfig {
@@ -71,9 +125,16 @@ impl AppConfig {
71 125 initial_size: (900, 560),
72 126 settings: ThemeSettings::default(),
73 127 system_dark: false,
128 + layer: None,
74 129 }
75 130 }
76 131
132 + /// Tworzy powierzchnię warstwową zamiast zwykłego okna.
133 + pub fn layer(mut self, layer: LayerConfig) -> Self {
134 + self.layer = Some(layer);
135 + self
136 + }
137 +
77 138 pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
78 139 self.app_id = app_id.into();
79 140 self
@@ -218,20 +279,57 @@ where
218 279
219 280 let compositor = CompositorState::bind(&globals, &qh)
220 281 .map_err(|e| format!("wl_compositor unavailable: {e}"))?;
221 - let xdg_shell =
222 - XdgShell::bind(&globals, &qh).map_err(|e| format!("xdg-shell unavailable: {e}"))?;
223 282 let shm = Shm::bind(&globals, &qh).map_err(|e| format!("wl_shm unavailable: {e}"))?;
224 283
225 284 let surface = compositor.create_surface(&qh);
226 - // CSD: sami rysujemy HeaderBar (brak dekoracji serwera).
227 - let window = xdg_shell.create_window(surface, WindowDecorations::ClientOnly, &qh);
228 - window.set_title(&config.title);
229 - window.set_app_id(&config.app_id);
230 - window.set_min_size(Some((380, 260)));
231 - // Pierwszy commit bez bufora — kompozytor odpowie konfiguracją.
232 - window.commit();
233 -
234 - let (iw, ih) = config.initial_size;
285 + // Dwa tryby: zwykłe okno `xdg-shell` (CSD) albo powierzchnia warstwowa (panel/dock).
286 + let target = match &config.layer {
287 + Some(layer_cfg) => {
288 + let layer_shell = LayerShell::bind(&globals, &qh)
289 + .map_err(|e| format!("wlr-layer-shell unavailable: {e}"))?;
290 + let ls = layer_shell.create_layer_surface(
291 + &qh,
292 + surface,
293 + layer_cfg.layer,
294 + Some(layer_cfg.namespace.clone()),
295 + None,
296 + );
297 + ls.set_anchor(layer_cfg.anchor);
298 + ls.set_size(layer_cfg.size.0, layer_cfg.size.1);
299 + ls.set_margin(
300 + layer_cfg.margin.0,
301 + layer_cfg.margin.1,
302 + layer_cfg.margin.2,
303 + layer_cfg.margin.3,
304 + );
305 + ls.set_exclusive_zone(layer_cfg.exclusive_zone);
306 + ls.set_keyboard_interactivity(if layer_cfg.keyboard {
307 + KeyboardInteractivity::OnDemand
308 + } else {
309 + KeyboardInteractivity::None
310 + });
311 + // Pierwszy commit bez bufora — kompozytor odpowie konfiguracją (docelowy rozmiar).
312 + ls.commit();
313 + Target::Layer(ls)
314 + }
315 + None => {
316 + let xdg_shell =
317 + XdgShell::bind(&globals, &qh).map_err(|e| format!("xdg-shell unavailable: {e}"))?;
318 + // CSD: sami rysujemy HeaderBar (brak dekoracji serwera).
319 + let window = xdg_shell.create_window(surface, WindowDecorations::ClientOnly, &qh);
320 + window.set_title(&config.title);
321 + window.set_app_id(&config.app_id);
322 + window.set_min_size(Some((380, 260)));
323 + // Pierwszy commit bez bufora — kompozytor odpowie konfiguracją.
324 + window.commit();
325 + Target::Window(window)
326 + }
327 + };
328 +
329 + let (iw, ih) = match &config.layer {
330 + Some(l) => (l.size.0.max(1), l.size.1.max(1)),
331 + None => config.initial_size,
332 + };
235 333 let pool = SlotPool::new((iw as usize * ih as usize * 4).max(1024), &shm)?;
236 334
237 335 let mut app = App {
@@ -247,7 +345,8 @@ where
247 345 scale: 1.0,
248 346 system_dark: config.system_dark,
249 347 buffer: None,
250 - window,
348 + target,
349 + configured: config.layer.is_none(),
251 350 keyboard: None,
252 351 pointer: None,
253 352 modifiers: Modifiers::NONE,
@@ -271,7 +370,7 @@ where
271 370 Duration::from_millis(100)
272 371 };
273 372 event_loop.dispatch(timeout, &mut app)?;
274 - if app.needs_redraw {
373 + if app.needs_redraw && app.configured {
275 374 app.redraw();
276 375 }
277 376 }
@@ -292,7 +391,9 @@ struct App {
292 391 scale: f64,
293 392 system_dark: bool,
294 393 buffer: Option<Buffer>,
295 - window: Window,
394 + target: Target,
395 + /// Powierzchnia warstwowa ma sens dopiero po pierwszej konfiguracji.
396 + configured: bool,
296 397 keyboard: Option<wl_keyboard::WlKeyboard>,
297 398 pointer: Option<wl_pointer::WlPointer>,
298 399 modifiers: Modifiers,
@@ -310,6 +411,28 @@ struct App {
310 411 maximized: bool,
311 412 }
312 413
414 +/// Powierzchnia docelowa: zwykłe okno albo powierzchnia warstwowa (panel/dock).
415 +enum Target {
416 + Window(Window),
417 + Layer(LayerSurface),
418 +}
419 +
420 +impl Target {
421 + fn surface(&self) -> &wl_surface::WlSurface {
422 + match self {
423 + Target::Window(w) => w.wl_surface(),
424 + Target::Layer(l) => l.wl_surface(),
425 + }
426 + }
427 +
428 + fn commit(&self) {
429 + match self {
430 + Target::Window(w) => w.commit(),
431 + Target::Layer(l) => l.commit(),
432 + }
433 + }
434 +}
435 +
313 436 impl App {
314 437 fn queue(&mut self, event: Event) {
315 438 self.pending_events.push(event);
@@ -357,15 +480,17 @@ impl App {
357 480 if self.actions.close {
358 481 self.exit = true;
359 482 }
360 - if self.actions.minimize {
361 - self.window.set_minimized();
362 - }
363 - if self.actions.toggle_maximize {
364 - self.maximized = !self.maximized;
365 - if self.maximized {
366 - self.window.set_maximized();
367 - } else {
368 - self.window.unset_maximized();
483 + if let Target::Window(window) = &self.target {
484 + if self.actions.minimize {
485 + window.set_minimized();
486 + }
487 + if self.actions.toggle_maximize {
488 + self.maximized = !self.maximized;
489 + if self.maximized {
490 + window.set_maximized();
491 + } else {
492 + window.unset_maximized();
493 + }
369 494 }
370 495 }
371 496 }
@@ -457,10 +582,10 @@ impl App {
457 582 dst[3] = src[3];
458 583 }
459 584
460 - let surface = self.window.wl_surface();
585 + let surface = self.target.surface();
461 586 surface.damage_buffer(0, 0, phys_w as i32, phys_h as i32);
462 587 buffer.attach_to(surface).expect("buffer attach");
463 - self.window.commit();
588 + self.target.commit();
464 589 }
465 590 }
466 591
@@ -559,6 +684,30 @@ impl WindowHandler for App {
559 684 }
560 685 }
561 686
687 +impl LayerShellHandler for App {
688 + fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
689 + self.exit = true;
690 + }
691 +
692 + fn configure(
693 + &mut self,
694 + _: &Connection,
695 + _: &QueueHandle<Self>,
696 + _layer: &LayerSurface,
697 + configure: LayerSurfaceConfigure,
698 + _serial: u32,
699 + ) {
700 + self.buffer = None;
701 + self.width = configure.new_size.0.max(1);
702 + self.height = configure.new_size.1.max(1);
703 + self.configured = true;
704 + self.queue(Event::Window(WindowEvent::Resize(Size::new(
705 + self.width as f64,
706 + self.height as f64,
707 + ))));
708 + }
709 +}
710 +
562 711 impl SeatHandler for App {
563 712 fn seat_state(&mut self) -> &mut SeatState {
564 713 &mut self.seat_state
@@ -694,7 +843,7 @@ impl PointerHandler for App {
694 843 events: &[SctkPointerEvent],
695 844 ) {
696 845 for event in events {
697 - if &event.surface != self.window.wl_surface() {
846 + if &event.surface != self.target.surface() {
698 847 continue;
699 848 }
700 849 let position = Point::new(event.position.0, event.position.1);
@@ -760,6 +909,7 @@ delegate_keyboard!(App);
760 909 delegate_pointer!(App);
761 910 delegate_xdg_shell!(App);
762 911 delegate_xdg_window!(App);
912 +delegate_layer!(App);
763 913 delegate_registry!(App);
764 914
765 915 impl ProvidesRegistryState for App {
@@ -70,9 +70,12 @@ Selene.
70 70 - `de/luna/src/panel.rs` — **panel górny** (menu, tytuł okna, zegar/data, przełączniki,
71 71 przestrzenie robocze, użytkownik) i **launcher** (siatka aplikacji + wyszukiwanie).
72 72 - `luna-panel` (bin) — render headless: `docs/images/luna-shell-{light,dark}.png`.
73 +- `luna-shell` (bin) — panel na żywo jako **powierzchnia warstwowa** (`wlr-layer-shell`).
74 +- `libluna-platform` obsługuje teraz oba tryby: okno `xdg-shell` (CSD) i powierzchnię
75 + warstwową (`AppConfig::layer(LayerConfig::…)`) — `LayerConfig::top_panel`/`overlay`.
73 76 - Dane (`PanelState`, `App`) są oddzielone od rysowania (`PaintCtx`), więc ten sam kod
74 77 obsługuje podglądy i sesję na żywo.
75 -- Dalej: panel jako powierzchnia `zwlr-layer-shell` (po stronie klienta), OSD, powiadomienia.
78 +- Dalej: OSD, powiadomienia, menu kontekstowe.
76 79
77 80 ## Jeszcze nie / Not yet
78 81
@@ -15,8 +15,13 @@ path = "src/lib.rs"
15 15 name = "luna-panel"
16 16 path = "src/bin/luna-panel.rs"
17 17
18 +[[bin]]
19 +name = "luna-shell"
20 +path = "src/bin/luna-shell.rs"
21 +
18 22 [dependencies]
19 23 libluna-core.workspace = true
20 24 libluna-widgets.workspace = true
21 25 libluna-render.workspace = true
22 26 libluna-i18n.workspace = true
27 +libluna-platform.workspace = true
23 28 new file mode 100644
@@ -0,0 +1,65 @@
1 +//! Panel Luny na żywo — powierzchnia warstwowa (`wlr-layer-shell`) na Selene.
2 +//!
3 +//! Uruchom w sesji Luny: `luna-shell` (albo `cargo run -p luna --bin luna-shell`).
4 +//! Wymaga kompozytora obsługującego `zwlr_layer_shell_v1` (Selene tak).
5 +
6 +use std::error::Error;
7 +use std::time::{SystemTime, UNIX_EPOCH};
8 +
9 +use libluna_core::{Rect, Theme};
10 +use libluna_platform::{run, AppConfig, Frame, LayerConfig};
11 +use libluna_widgets::PaintCtx;
12 +use luna::panel::{self, PanelState, THICKNESS};
13 +
14 +fn main() -> Result<(), Box<dyn Error>> {
15 + let cfg = AppConfig::new("Luna Panel")
16 + .app_id("eu.paganlinux.Luna.Panel")
17 + .layer(LayerConfig::top_panel("luna-panel", THICKNESS as u32, true));
18 +
19 + let mut state = PanelState::sample();
20 +
21 + run(cfg, move |frame: &mut Frame| {
22 + // Żywy zegar (UTC) — panel odświeża się w stałym rytmie.
23 + let (clock, date) = now_clock();
24 + state.clock = clock;
25 + state.date = date;
26 +
27 + let size = frame.size;
28 + let theme: &Theme = &frame.theme;
29 + let mut ctx = PaintCtx::new(frame.scene, theme, frame.text);
30 + panel::paint_panel(
31 + &state,
32 + &mut ctx,
33 + Rect::from_xywh(0.0, 0.0, size.width, THICKNESS),
34 + );
35 +
36 + frame.request_frame();
37 + })
38 +}
39 +
40 +/// Bieżąca godzina i data w UTC (bez zależności na TZ) — `("HH:MM", "DD.MM")`.
41 +fn now_clock() -> (String, String) {
42 + let secs = SystemTime::now()
43 + .duration_since(UNIX_EPOCH)
44 + .map(|d| d.as_secs())
45 + .unwrap_or(0);
46 + let days = (secs / 86_400) as i64;
47 + let secs_of_day = secs % 86_400;
48 + let (h, m) = (secs_of_day / 3600, (secs_of_day % 3600) / 60);
49 + let (_, mo, d) = civil_from_days(days);
50 + (format!("{h:02}:{m:02}"), format!("{d:02}.{mo:02}"))
51 +}
52 +
53 +/// Data cywilna (rok, miesiąc, dzień) z liczby dni od 1970-01-01 (algorytm Hinnanta).
54 +fn civil_from_days(z: i64) -> (i64, u32, u32) {
55 + let z = z + 719_468;
56 + let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
57 + let doe = z - era * 146_097;
58 + let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
59 + let y = yoe + era * 400;
60 + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
61 + let mp = (5 * doy + 2) / 153;
62 + let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
63 + let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
64 + (if m <= 2 { y + 1 } else { y }, m, d)
65 +}
@@ -164,8 +164,9 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
164 164 (menu, tytuł okna, zegar/data, przełączniki, przestrzenie, użytkownik) i **launcher**
165 165 (siatka aplikacji + wyszukiwanie); render headless do `docs/images/luna-shell-{light,dark}.png`.
166 166 Dane (`PanelState`, `App`) oddzielone od rysowania (`PaintCtx`) — ten sam kod do podglądów
167 - i sesji na żywo. Dalej: powierzchnia warstwowa `zwlr-layer-shell` po stronie klienta
168 - (panel jako prawdziwy layer surface na Selene), OSD, menu kontekstowe, powiadomienia.
167 + i sesji na żywo. ✅ **`luna-shell`** (bin) uruchamia panel jako prawdziwą **powierzchnię
168 + warstwową** (`wlr-layer-shell` po stronie klienta w `libluna-platform`); bez kompozytora
169 + kończy się czytelnym `NoCompositor`. Dalej: OSD, powiadomienia, menu kontekstowe.
169 170
170 171 ## Później (poza MVP)
171 172 - Dostępność AT-SPI, IME, schowek, drag & drop, animacje, backend GPU, mini-CSS dla motywów.