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.