Commit ded37f4
10
files
+479
added
-2
deleted
@@ -296,6 +296,16 @@ version = "0.4.34"
296
296
source = "registry+https://github.com/rust-lang/crates.io-index"
297
297
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
298
298
299
+[[package]]
300
+name = "luna"
301
+version = "0.0.1"
302
+dependencies = [
303
+ "libluna-core",
304
+ "libluna-i18n",
305
+ "libluna-render",
306
+ "libluna-widgets",
307
+]
308
+
299
309
[[package]]
300
310
name = "luna-demo"
301
311
version = "0.0.1"
@@ -8,6 +8,7 @@ members = [
8
8
"crates/libluna-platform",
9
9
"crates/libluna-widgets",
10
10
"apps/luna-demo",
11
+ "de/luna",
11
12
"de/selene",
12
13
]
13
14
@@ -16,7 +16,7 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
16
16
| Warstwa / Layer | Nazwa / Name | Opis |
17
17
| --- | --- | --- |
18
18
| Toolkit GUI | **libLuna** | widgety, render, i18n (`crates/libluna-*`) |
19
-| Środowisko | **Luna** | pulpit (panel, launcher, ustawienia) — `de/luna` (wkrótce) |
19
+| Środowisko | **Luna** | pulpit (panel, launcher, OSD) — `de/luna` |
20
20
| Kompozytor | **Selene** | serwer Wayland — `de/selene` |
21
21
22
22
## Co już działa / What works (M-K5)
@@ -62,6 +62,18 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
62
62
fallback do nested (nie odbiera ekranu).
63
63
- Wielojęzyczność (`libluna-i18n`) i optymalizacja (`[profile.release]` LTO, cache `shm`).
64
64
65
+## Powłoka Luna / Luna shell (`de/luna`)
66
+
67
+Środowisko graficzne Luna (odpowiednik GNOME Shell) na libLunie, działające na kompozytorze
68
+Selene.
69
+
70
+- `de/luna/src/panel.rs` — **panel górny** (menu, tytuł okna, zegar/data, przełączniki,
71
+ przestrzenie robocze, użytkownik) i **launcher** (siatka aplikacji + wyszukiwanie).
72
+- `luna-panel` (bin) — render headless: `docs/images/luna-shell-{light,dark}.png`.
73
+- Dane (`PanelState`, `App`) są oddzielone od rysowania (`PaintCtx`), więc ten sam kod
74
+ obsługuje podglądy i sesję na żywo.
75
+- Dalej: panel jako powierzchnia `zwlr-layer-shell` (po stronie klienta), OSD, powiadomienia.
76
+
65
77
## Jeszcze nie / Not yet
66
78
67
79
- **Wejście w trybie DRM** (evdev/`libinput`) — skanowanie działa, ale bez rodzimego wejścia
68
80
new file mode 100644
@@ -0,0 +1,22 @@
1
+[package]
2
+name = "luna"
3
+version.workspace = true
4
+edition.workspace = true
5
+license.workspace = true
6
+rust-version.workspace = true
7
+authors.workspace = true
8
+description = "Luna — powłoka środowiska graficznego (panel, launcher) na libLunie, działająca na kompozytorze Selene."
9
+
10
+[lib]
11
+name = "luna"
12
+path = "src/lib.rs"
13
+
14
+[[bin]]
15
+name = "luna-panel"
16
+path = "src/bin/luna-panel.rs"
17
+
18
+[dependencies]
19
+libluna-core.workspace = true
20
+libluna-widgets.workspace = true
21
+libluna-render.workspace = true
22
+libluna-i18n.workspace = true
0
23
new file mode 100644
@@ -0,0 +1,35 @@
1
+//! Podgląd powłoki Luny (headless): panel + launcher, w motywie jasnym i ciemnym.
2
+//!
3
+//! Uruchom: `cargo run -p luna --bin luna-panel`
4
+//! Efekt: `docs/images/luna-shell-light.png`, `docs/images/luna-shell-dark.png`
5
+
6
+use std::error::Error;
7
+
8
+use libluna_core::{Size, Theme};
9
+use libluna_render::{Scene, SoftwareRenderer, TextEngine};
10
+use libluna_widgets::PaintCtx;
11
+use luna::panel::{self, App, PanelState};
12
+
13
+const W: u32 = 1280;
14
+const H: u32 = 720;
15
+
16
+fn main() -> Result<(), Box<dyn Error>> {
17
+ let text = TextEngine::system();
18
+ let size = Size::new(W as f64, H as f64);
19
+ let panel = PanelState::sample();
20
+ let apps = App::sample();
21
+
22
+ for (name, theme) in [("light", Theme::light()), ("dark", Theme::dark())] {
23
+ let mut scene = Scene::new();
24
+ let mut paint = PaintCtx::new(&mut scene, &theme, &text);
25
+ panel::paint_desktop(&panel, &apps, &mut paint, size);
26
+
27
+ let mut renderer = SoftwareRenderer::new(W, H);
28
+ renderer.draw(&scene, 1.0, theme.colors.window_bg);
29
+ let png = renderer.encode_png().ok_or("failed to encode PNG")?;
30
+ let path = format!("docs/images/luna-shell-{name}.png");
31
+ std::fs::write(&path, png)?;
32
+ println!("saved {path} ({W}x{H})");
33
+ }
34
+ Ok(())
35
+}
0
36
new file mode 100644
@@ -0,0 +1,9 @@
1
+//! # luna
2
+//!
3
+//! Powłoka środowiska graficznego **Luna** — odpowiednik GNOME Shell, ale zbudowany
4
+//! na libLunie i działający na kompozytorze **Selene**.
5
+//!
6
+//! Moduły rysują się przez `libluna-widgets::PaintCtx`, więc ten sam kod obsługuje
7
+//! podglądy headless (zrzuty do dokumentacji) i sesję na żywo.
8
+
9
+pub mod panel;
0
10
new file mode 100644
@@ -0,0 +1,383 @@
1
+//! Panel górny i launcher środowiska Luna.
2
+//!
3
+//! Warstwa rysowania jest oddzielona od źródła danych ([`PanelState`], [`App`]), więc
4
+//! panel można renderować headless (podglądy) oraz „na żywo” w sesji Selene.
5
+
6
+use libluna_core::style::weight;
7
+use libluna_core::{Color, Point, Rect, Size, Theme};
8
+use libluna_widgets::PaintCtx;
9
+
10
+/// Wysokość panelu (piksele logiczne).
11
+pub const THICKNESS: f64 = 40.0;
12
+
13
+/// Przełącznik szybkiego dostępu (Wi-Fi, dźwięk, jasność…).
14
+#[derive(Debug, Clone)]
15
+pub struct Toggle {
16
+ pub label: String,
17
+ /// Symbol rysowany fontem interfejsu (może być pusty).
18
+ pub glyph: String,
19
+ pub active: bool,
20
+}
21
+
22
+/// Stan panelu — czyste dane, bez logiki okna (łatwe do testów i podglądów).
23
+#[derive(Debug, Clone)]
24
+pub struct PanelState {
25
+ /// Etykieta przycisku menu (np. „Aktywności”).
26
+ pub menu_label: String,
27
+ /// Tytuł aktywnego okna lub nazwa przestrzeni.
28
+ pub title: String,
29
+ /// Godzina, np. „14:32”.
30
+ pub clock: String,
31
+ /// Data, np. „pon, 10 paź”.
32
+ pub date: String,
33
+ /// Liczba przestrzeni roboczych.
34
+ pub workspaces: usize,
35
+ /// Indeks bieżącej przestrzeni roboczej.
36
+ pub current_ws: usize,
37
+ /// Przełączniki szybkiego dostępu.
38
+ pub toggles: Vec<Toggle>,
39
+ /// Nazwa zalogowanego użytkownika.
40
+ pub user: String,
41
+}
42
+
43
+impl PanelState {
44
+ /// Przykładowy stan (podglądy, testy).
45
+ pub fn sample() -> Self {
46
+ Self {
47
+ menu_label: "Aktywności".to_owned(),
48
+ title: "Pliki — Dokumenty".to_owned(),
49
+ clock: "14:32".to_owned(),
50
+ date: "pon, 10 paź".to_owned(),
51
+ workspaces: 4,
52
+ current_ws: 1,
53
+ toggles: vec![
54
+ Toggle {
55
+ label: "Wi-Fi".to_owned(),
56
+ glyph: "▲".to_owned(),
57
+ active: true,
58
+ },
59
+ Toggle {
60
+ label: "Dźwięk".to_owned(),
61
+ glyph: "◆".to_owned(),
62
+ active: true,
63
+ },
64
+ Toggle {
65
+ label: "82%".to_owned(),
66
+ glyph: "■".to_owned(),
67
+ active: false,
68
+ },
69
+ ],
70
+ user: "patryks".to_owned(),
71
+ }
72
+ }
73
+}
74
+
75
+/// Pozycje w launcherze.
76
+#[derive(Debug, Clone)]
77
+pub struct App {
78
+ /// Symbol rysowany fontem interfejsu.
79
+ pub glyph: String,
80
+ pub name: String,
81
+}
82
+
83
+impl App {
84
+ /// Przykładowy zestaw ikon (podglądy).
85
+ pub fn sample() -> Vec<App> {
86
+ const APPS: [(&str, &str); 8] = [
87
+ ("❖", "Pliki"),
88
+ ("▦", "Ustawienia"),
89
+ ("✎", "Edytor"),
90
+ ("▶", "Odtwarzacz"),
91
+ ("★", "Ulubione"),
92
+ ("◔", "Zegar"),
93
+ ("⌗", "Terminal"),
94
+ ("✦", "Sklep"),
95
+ ];
96
+ APPS.iter()
97
+ .map(|(g, n)| App {
98
+ glyph: (*g).to_owned(),
99
+ name: (*n).to_owned(),
100
+ })
101
+ .collect()
102
+ }
103
+}
104
+
105
+/// Rysuje panel górny w zadanym prostokącie.
106
+pub fn paint_panel(panel: &PanelState, ctx: &mut PaintCtx, rect: Rect) {
107
+ let t = ctx.theme;
108
+ let cy = rect.center().y;
109
+ let body = t.typography.body.size;
110
+ let pad = t.spacing.md;
111
+
112
+ // Tło + dolna krawędź oddzielająca od pulpitu.
113
+ ctx.scene.fill_rect(rect, t.colors.headerbar_bg);
114
+ ctx.scene.fill_rect(
115
+ Rect::from_xywh(rect.x(), rect.bottom() - 1.0, rect.width(), 1.0),
116
+ t.colors.border,
117
+ );
118
+
119
+ // ── Lewa grupa: przycisk menu + tytuł + separator ──
120
+ let menu = Rect::from_xywh(rect.x() + pad, cy - 12.0, 24.0, 24.0);
121
+ ctx.scene
122
+ .fill_rounded_rect(menu, t.radius.small, t.colors.shade);
123
+ ctx.label_center(
124
+ menu.center().x,
125
+ menu.center().y,
126
+ "☰",
127
+ body,
128
+ weight::BOLD,
129
+ t.colors.text,
130
+ );
131
+ let mut x = menu.right() + t.spacing.sm;
132
+ ctx.label_left(
133
+ x,
134
+ cy,
135
+ &panel.menu_label,
136
+ body,
137
+ weight::MEDIUM,
138
+ t.colors.text,
139
+ );
140
+ x += ctx.measure(&panel.menu_label, body, weight::MEDIUM) + t.spacing.md;
141
+ ctx.scene
142
+ .fill_rect(Rect::from_xywh(x, cy - 8.0, 1.0, 16.0), t.colors.border);
143
+ x += t.spacing.md;
144
+ ctx.label_left(x, cy, &panel.title, body, weight::NORMAL, t.colors.dim_text);
145
+
146
+ // ── Środek: zegar + data ──
147
+ let clock = format!("{} · {}", panel.clock, panel.date);
148
+ ctx.label_center(
149
+ rect.center().x,
150
+ cy,
151
+ &clock,
152
+ body,
153
+ weight::MEDIUM,
154
+ t.colors.text,
155
+ );
156
+
157
+ // ── Prawa grupa (od prawej): użytkownik, przełączniki, przestrzenie ──
158
+ let mut right = rect.right() - pad;
159
+ let uw = ctx.measure(&panel.user, body, weight::NORMAL);
160
+ ctx.label_right(right, cy, &panel.user, body, weight::NORMAL, t.colors.text);
161
+ right -= uw + t.spacing.md;
162
+ ctx.scene
163
+ .fill_rect(Rect::from_xywh(right, cy - 8.0, 1.0, 16.0), t.colors.border);
164
+ right -= t.spacing.md;
165
+
166
+ for toggle in panel.toggles.iter().rev() {
167
+ let label = if toggle.glyph.is_empty() {
168
+ toggle.label.clone()
169
+ } else {
170
+ format!("{} {}", toggle.glyph, toggle.label)
171
+ };
172
+ let cap = t.typography.caption.size;
173
+ let w = ctx.measure(&label, cap, weight::MEDIUM) + t.spacing.lg;
174
+ let pill = Rect::from_xywh(right - w, cy - 12.0, w, 24.0);
175
+ paint_pill(ctx, pill, &label, toggle.active);
176
+ right = pill.x() - t.spacing.sm;
177
+ }
178
+
179
+ for i in (0..panel.workspaces).rev() {
180
+ let d = 8.0;
181
+ let dot = Rect::from_xywh(right - d, cy - d / 2.0, d, d);
182
+ let color = if i == panel.current_ws {
183
+ t.colors.accent
184
+ } else {
185
+ t.colors.dim_text
186
+ };
187
+ ctx.scene.fill_rounded_rect(dot, d / 2.0, color);
188
+ right = dot.x() - t.spacing.sm;
189
+ }
190
+}
191
+
192
+/// Pigułka przełącznika (aktywna = akcent).
193
+fn paint_pill(ctx: &mut PaintCtx, rect: Rect, label: &str, active: bool) {
194
+ let t = ctx.theme;
195
+ let bg = if active {
196
+ t.colors.accent
197
+ } else {
198
+ t.colors.card_bg
199
+ };
200
+ let fg = if active {
201
+ t.colors.accent_fg
202
+ } else {
203
+ t.colors.text
204
+ };
205
+ ctx.scene.fill_rounded_rect(rect, rect.height() / 2.0, bg);
206
+ if !active {
207
+ ctx.scene
208
+ .stroke_rounded_rect(rect, rect.height() / 2.0, 1.0, t.colors.border);
209
+ }
210
+ ctx.label_center(
211
+ rect.center().x,
212
+ rect.center().y,
213
+ label,
214
+ t.typography.caption.size,
215
+ weight::MEDIUM,
216
+ fg,
217
+ );
218
+}
219
+
220
+/// Rysuje launcher (menu aplikacji) w zadanym prostokącie.
221
+pub fn paint_launcher(apps: &[App], ctx: &mut PaintCtx, rect: Rect) {
222
+ let t = ctx.theme;
223
+ ctx.scene
224
+ .fill_rounded_rect(rect, t.radius.large, t.colors.card_bg);
225
+ ctx.scene
226
+ .stroke_rounded_rect(rect, t.radius.large, 1.0, t.colors.border);
227
+
228
+ let pad = t.spacing.lg;
229
+ ctx.label_left(
230
+ rect.x() + pad,
231
+ rect.y() + pad + 6.0,
232
+ "Aplikacje",
233
+ t.typography.heading.size,
234
+ weight::BOLD,
235
+ t.colors.text,
236
+ );
237
+
238
+ // Pole wyszukiwania (makieta).
239
+ let search = Rect::from_xywh(
240
+ rect.x() + pad,
241
+ rect.y() + pad + 28.0,
242
+ rect.width() - 2.0 * pad,
243
+ 32.0,
244
+ );
245
+ ctx.scene
246
+ .fill_rounded_rect(search, t.radius.medium, t.colors.view_bg);
247
+ ctx.scene
248
+ .stroke_rounded_rect(search, t.radius.medium, 1.0, t.colors.border);
249
+ ctx.label_left(
250
+ search.x() + t.spacing.md,
251
+ search.center().y,
252
+ "Szukaj…",
253
+ t.typography.body.size,
254
+ weight::NORMAL,
255
+ t.colors.dim_text,
256
+ );
257
+
258
+ // Siatka ikon (4 kolumny).
259
+ let cols = 4usize;
260
+ let gap = t.spacing.md;
261
+ let gx = rect.x() + pad;
262
+ let gy = search.bottom() + t.spacing.lg;
263
+ let cell_w = (rect.width() - 2.0 * pad - gap * (cols as f64 - 1.0)) / cols as f64;
264
+ let cell_h = 76.0;
265
+ for (i, app) in apps.iter().enumerate() {
266
+ let c = (i % cols) as f64;
267
+ let r = (i / cols) as f64;
268
+ let tile = Rect::from_xywh(
269
+ gx + c * (cell_w + gap),
270
+ gy + r * (cell_h + gap),
271
+ cell_w,
272
+ cell_h,
273
+ );
274
+ paint_app_tile(app, ctx, tile);
275
+ }
276
+}
277
+
278
+/// Kafelek aplikacji: ikona + nazwa.
279
+fn paint_app_tile(app: &App, ctx: &mut PaintCtx, rect: Rect) {
280
+ let t = ctx.theme;
281
+ ctx.scene
282
+ .fill_rounded_rect(rect, t.radius.medium, t.colors.view_bg);
283
+ ctx.scene
284
+ .stroke_rounded_rect(rect, t.radius.medium, 1.0, t.colors.border);
285
+
286
+ // Kwadratowa ikona w akcencie (delikatnym), z symbolem.
287
+ let icon = Rect::from_xywh(rect.center().x - 18.0, rect.y() + 12.0, 36.0, 36.0);
288
+ ctx.scene
289
+ .fill_rounded_rect(icon, t.radius.small, t.colors.accent.scale_alpha(0.16));
290
+ ctx.label_center(
291
+ icon.center().x,
292
+ icon.center().y,
293
+ &app.glyph,
294
+ 18.0,
295
+ weight::MEDIUM,
296
+ t.colors.accent,
297
+ );
298
+ ctx.label_center(
299
+ rect.center().x,
300
+ icon.bottom() + 12.0,
301
+ &app.name,
302
+ t.typography.caption.size,
303
+ weight::NORMAL,
304
+ t.colors.text,
305
+ );
306
+}
307
+
308
+/// Pełny podgląd pulpitu: tapeta, panel górny i launcher.
309
+pub fn paint_desktop(panel: &PanelState, apps: &[App], ctx: &mut PaintCtx, size: Size) {
310
+ let accent = ctx.theme.colors.accent;
311
+
312
+ // Tapeta: tło + delikatny motyw z akcentu.
313
+ ctx.scene
314
+ .fill_rect(Rect::from_size(size), ctx.theme.colors.window_bg);
315
+ let blob = Point::new(size.width * 0.78, size.height * 0.74);
316
+ ctx.scene.fill_rounded_rect(
317
+ Rect::from_xywh(blob.x - 220.0, blob.y - 220.0, 440.0, 440.0),
318
+ 220.0,
319
+ accent.scale_alpha(0.10),
320
+ );
321
+
322
+ paint_panel(panel, ctx, Rect::from_xywh(0.0, 0.0, size.width, THICKNESS));
323
+
324
+ let lw = 620.0;
325
+ let lh = 360.0;
326
+ let lrect = Rect::from_xywh(
327
+ (size.width - lw) / 2.0,
328
+ (size.height - lh) / 2.0 + 16.0,
329
+ lw,
330
+ lh,
331
+ );
332
+ paint_launcher(apps, ctx, lrect);
333
+}
334
+
335
+/// Wyciszony kolor akcentu (pomocniczo dla tapet/podświetleń).
336
+pub fn accent_soft(theme: &Theme) -> Color {
337
+ theme.colors.accent.scale_alpha(0.12)
338
+}
339
+
340
+#[cfg(test)]
341
+mod tests {
342
+ use super::*;
343
+ use libluna_render::{Scene, TextEngine};
344
+
345
+ fn panel_commands(apps: usize) -> usize {
346
+ let theme = Theme::light();
347
+ let text = TextEngine::none();
348
+ let mut scene = Scene::new();
349
+ let mut ctx = PaintCtx::new(&mut scene, &theme, &text);
350
+ let panel = PanelState::sample();
351
+ paint_panel(
352
+ &panel,
353
+ &mut ctx,
354
+ Rect::from_xywh(0.0, 0.0, 1280.0, THICKNESS),
355
+ );
356
+ if apps > 0 {
357
+ paint_launcher(
358
+ &App::sample()[..apps],
359
+ &mut ctx,
360
+ Rect::from_xywh(0.0, 0.0, 620.0, 360.0),
361
+ );
362
+ }
363
+ scene.len()
364
+ }
365
+
366
+ #[test]
367
+ fn sample_state_is_consistent() {
368
+ let s = PanelState::sample();
369
+ assert!(s.workspaces > 0);
370
+ assert!(s.current_ws < s.workspaces);
371
+ assert!(!s.toggles.is_empty());
372
+ }
373
+
374
+ #[test]
375
+ fn panel_emits_draw_commands() {
376
+ assert!(panel_commands(0) > 10);
377
+ }
378
+
379
+ #[test]
380
+ fn launcher_draws_more_with_more_apps() {
381
+ assert!(panel_commands(8) > panel_commands(4));
382
+ }
383
+}
0
384
new file mode 100644
1
385
Binary files /dev/null and b/docs/images/luna-shell-dark.png differ
2
386
new file mode 100644
3
387
Binary files /dev/null and b/docs/images/luna-shell-light.png differ
@@ -160,7 +160,12 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
160
160
Uruchomienie: `cargo run -p selene` (szczegóły: `de/README.md`).
161
161
- Dalej: **wejście w trybie DRM** (evdev/`libinput`), GLES/akceleracja, hotplug monitorów
162
162
(`zwlr_output_manager`), selekcja pierwotna, IME (`text-input`).
163
-- **Luna** (`de/luna`) — powłoka środowiska (panel, launcher, OSD) na `libluna-widgets`.
163
+- **Luna** (`de/luna`) — powłoka środowiska na `libluna-widgets`. ✅ start: **panel górny**
164
+ (menu, tytuł okna, zegar/data, przełączniki, przestrzenie, użytkownik) i **launcher**
165
+ (siatka aplikacji + wyszukiwanie); render headless do `docs/images/luna-shell-{light,dark}.png`.
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.
164
169
165
170
## Później (poza MVP)
166
171
- Dostępność AT-SPI, IME, schowek, drag & drop, animacje, backend GPU, mini-CSS dla motywów.