← luna

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.