Commit ab03027
10
files
+508
added
-20
deleted
@@ -12,7 +12,7 @@ use std::error::Error;
12
12
use std::time::{Duration, Instant};
13
13
14
14
use libluna_core::{
15
- Accent, ButtonState, Event, KeyEvent, Modifiers, Point, PointerButton, PointerEvent,
15
+ Accent, ButtonState, Color, Event, KeyEvent, Modifiers, Point, PointerButton, PointerEvent,
16
16
ScrollEvent, Size, Theme, ThemePreference, ThemeSettings, WindowEvent,
17
17
};
18
18
use libluna_render::{Scene, SoftwareRenderer, TextEngine};
@@ -68,6 +68,8 @@ pub struct AppConfig {
68
68
/// Gdy ustawione — zamiast okna `xdg-shell` tworzymy powierzchnię `wlr-layer-shell`
69
69
/// (panel/dock/tło). To tryb używany przez powłokę Luna.
70
70
pub layer: Option<LayerConfig>,
71
+ /// Przezroczyste tło powierzchni (dla paneli/OSD warstwowych).
72
+ pub transparent: bool,
71
73
}
72
74
73
75
/// Konfiguracja powierzchni warstwowej (`wlr-layer-shell`).
@@ -115,6 +117,20 @@ impl LayerConfig {
115
117
keyboard: false,
116
118
}
117
119
}
120
+
121
+ /// Powierzchnia „chrome”: pasek o grubości `bar` plus przezroczysty obszar pod nim
122
+ /// (na OSD i powiadomienia). Strefa wyłączna obejmuje tylko pasek.
123
+ pub fn top_chrome(namespace: impl Into<String>, bar: u32, total: u32) -> Self {
124
+ Self {
125
+ namespace: namespace.into(),
126
+ layer: Layer::Top,
127
+ anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
128
+ size: (0, total),
129
+ margin: (0, 0, 0, 0),
130
+ exclusive_zone: bar as i32,
131
+ keyboard: true,
132
+ }
133
+ }
118
134
}
119
135
120
136
impl AppConfig {
@@ -126,6 +142,7 @@ impl AppConfig {
126
142
settings: ThemeSettings::default(),
127
143
system_dark: false,
128
144
layer: None,
145
+ transparent: false,
129
146
}
130
147
}
131
148
@@ -135,6 +152,12 @@ impl AppConfig {
135
152
self
136
153
}
137
154
155
+ /// Przezroczyste tło (panel/OSD warstwowy).
156
+ pub fn transparent(mut self, transparent: bool) -> Self {
157
+ self.transparent = transparent;
158
+ self
159
+ }
160
+
138
161
pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
139
162
self.app_id = app_id.into();
140
163
self
@@ -344,6 +367,7 @@ where
344
367
height: ih,
345
368
scale: 1.0,
346
369
system_dark: config.system_dark,
370
+ transparent: config.transparent,
347
371
buffer: None,
348
372
target,
349
373
configured: config.layer.is_none(),
@@ -390,6 +414,8 @@ struct App {
390
414
height: u32,
391
415
scale: f64,
392
416
system_dark: bool,
417
+ /// Czy tło powierzchni ma być przezroczyste (panele/OSD).
418
+ transparent: bool,
393
419
buffer: Option<Buffer>,
394
420
target: Target,
395
421
/// Powierzchnia warstwowa ma sens dopiero po pierwszej konfiguracji.
@@ -528,8 +554,13 @@ impl App {
528
554
render(&mut frame);
529
555
}
530
556
531
- self.renderer
532
- .draw(&self.scene, scale, theme.colors.window_bg);
557
+ // Panele/OSD warstwowe mają przezroczyste tło (widać pulpit pod spodem).
558
+ let bg = if self.transparent {
559
+ Color::rgba(0.0, 0.0, 0.0, 0.0)
560
+ } else {
561
+ theme.colors.window_bg
562
+ };
563
+ self.renderer.draw(&self.scene, scale, bg);
533
564
}
534
565
535
566
fn present(&mut self) {
@@ -70,12 +70,14 @@ 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`).
73
+- `luna-shell` (bin) — powierzchnia „chrome” na żywo: pasek + **OSD** + **powiadomienia**
74
+ (`osd.rs`, `notifications.rs`; animacja, auto-zamknięcie).
74
75
- `libluna-platform` obsługuje teraz oba tryby: okno `xdg-shell` (CSD) i powierzchnię
75
- warstwową (`AppConfig::layer(LayerConfig::…)`) — `LayerConfig::top_panel`/`overlay`.
76
+ warstwową (`AppConfig::layer(LayerConfig::…)`) — `LayerConfig::top_panel`/`overlay`/`top_chrome`
77
+ oraz przezroczyste tło (`AppConfig::transparent`).
76
78
- Dane (`PanelState`, `App`) są oddzielone od rysowania (`PaintCtx`), więc ten sam kod
77
79
obsługuje podglądy i sesję na żywo.
78
-- Dalej: OSD, powiadomienia, menu kontekstowe.
80
+- Dalej: usługa D-Bus `org.freedesktop.Notifications`, centrum powiadomień, menu kontekstowe.
79
81
80
82
## Jeszcze nie / Not yet
81
83
@@ -1,13 +1,15 @@
1
-//! Podgląd powłoki Luny (headless): panel + launcher, w motywie jasnym i ciemnym.
1
+//! Podgląd powłoki Luny (headless): panel, OSD i powiadomienia — jasny i ciemny motyw.
2
2
//!
3
3
//! Uruchom: `cargo run -p luna --bin luna-panel`
4
4
//! Efekt: `docs/images/luna-shell-light.png`, `docs/images/luna-shell-dark.png`
5
5
6
6
use std::error::Error;
7
7
8
-use libluna_core::{Size, Theme};
8
+use libluna_core::{Rect, Size, Theme};
9
9
use libluna_render::{Scene, SoftwareRenderer, TextEngine};
10
10
use libluna_widgets::PaintCtx;
11
+use luna::notifications::{Notification, Notifications};
12
+use luna::osd::Osd;
11
13
use luna::panel::{self, App, PanelState};
12
14
13
15
const W: u32 = 1280;
@@ -16,13 +18,33 @@ const H: u32 = 720;
16
18
fn main() -> Result<(), Box<dyn Error>> {
17
19
let text = TextEngine::system();
18
20
let size = Size::new(W as f64, H as f64);
19
- let panel = PanelState::sample();
21
+ let pstate = PanelState::sample();
20
22
let apps = App::sample();
21
23
22
24
for (name, theme) in [("light", Theme::light()), ("dark", Theme::dark())] {
23
25
let mut scene = Scene::new();
24
26
let mut paint = PaintCtx::new(&mut scene, &theme, &text);
25
- panel::paint_desktop(&panel, &apps, &mut paint, size);
27
+
28
+ // Pulpit + panel + launcher.
29
+ panel::paint_desktop(&pstate, &apps, &mut paint, size);
30
+
31
+ // OSD wyśrodkowany pod paskiem (po animacji wejścia).
32
+ let mut osd = Osd::new("◆", "Głośność", 0.65);
33
+ osd.advance(0.3);
34
+ osd.paint(
35
+ &mut paint,
36
+ Rect::from_xywh((W as f64 - 340.0) / 2.0, 64.0, 340.0, 72.0),
37
+ );
38
+
39
+ // Powiadomienia w prawym-górnym narożniku.
40
+ let mut notes = Notifications::new();
41
+ notes.push(
42
+ Notification::new("Pliki", "Kopiowanie zakończone", "12 plików → Dokumenty")
43
+ .with_glyph("▦"),
44
+ );
45
+ notes.push(Notification::new("System", "Bateria", "Pozostało 18%").with_glyph("■"));
46
+ notes.advance(0.3);
47
+ notes.paint(&mut paint, Rect::from_xywh(0.0, 56.0, W as f64, 240.0));
26
48
27
49
let mut renderer = SoftwareRenderer::new(W, H);
28
50
renderer.draw(&scene, 1.0, theme.colors.window_bg);
@@ -1,7 +1,9 @@
1
-//! Panel Luny na żywo — powierzchnia warstwowa (`wlr-layer-shell`) na Selene.
1
+//! Powłoka Luny na żywo — pasek, OSD i powiadomienia jako powierzchnia warstwowa.
2
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).
3
+//! `luna-shell` tworzy powierzchnię „chrome" (`wlr-layer-shell`): pasek na górze,
4
+//! a pod nim przezroczysty obszar na OSD i powiadomienia. Bez usługi D-Bus
5
+//! `org.freedesktop.Notifications` (kolejny krok) demo samo cyklicznie pokazuje OSD
6
+//! i powiadomienia, żeby warstwę prezentacji dało się zobaczyć na żywo.
5
7
6
8
use std::error::Error;
7
9
use std::time::{SystemTime, UNIX_EPOCH};
@@ -9,35 +11,79 @@ use std::time::{SystemTime, UNIX_EPOCH};
9
11
use libluna_core::{Rect, Theme};
10
12
use libluna_platform::{run, AppConfig, Frame, LayerConfig};
11
13
use libluna_widgets::PaintCtx;
14
+use luna::notifications::{Notification, Notifications};
15
+use luna::osd::Osd;
12
16
use luna::panel::{self, PanelState, THICKNESS};
13
17
18
+/// Wysokość powierzchni „chrome" (pasek + obszar nakładek).
19
+const CHROME_H: u32 = 240;
20
+
14
21
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));
22
+ let cfg = AppConfig::new("Luna Shell")
23
+ .app_id("eu.paganlinux.Luna.Shell")
24
+ .layer(LayerConfig::top_chrome(
25
+ "luna-shell",
26
+ THICKNESS as u32,
27
+ CHROME_H,
28
+ ))
29
+ .transparent(true);
18
30
19
31
let mut state = PanelState::sample();
32
+ let mut osd = Osd::new("◆", "Głośność", 0.0);
33
+ let mut notes = Notifications::new();
34
+ let mut timer = 0.0f64;
20
35
21
36
run(cfg, move |frame: &mut Frame| {
22
- // Żywy zegar (UTC) — panel odświeża się w stałym rytmie.
37
+ let dt = frame.delta_time();
38
+ timer += dt;
39
+
40
+ // Zegar (UTC).
23
41
let (clock, date) = now_clock();
24
42
state.clock = clock;
25
43
state.date = date;
26
44
45
+ // Demo: co ~4 s OSD, co ~7 s powiadomienie.
46
+ if timer % 7.0 < dt {
47
+ let value = 0.15 + (timer as i64 % 8) as f64 / 10.0;
48
+ osd.show("Głośność", value);
49
+ }
50
+ if timer % 4.0 < dt {
51
+ notes.push(
52
+ Notification::new("Pliki", "Kopiowanie zakończone", "12 plików → Dokumenty")
53
+ .with_glyph("▦"),
54
+ );
55
+ }
56
+ osd.advance(dt);
57
+ notes.advance(dt);
58
+
27
59
let size = frame.size;
28
60
let theme: &Theme = &frame.theme;
29
61
let mut ctx = PaintCtx::new(frame.scene, theme, frame.text);
62
+ // Pasek na górze.
30
63
panel::paint_panel(
31
64
&state,
32
65
&mut ctx,
33
66
Rect::from_xywh(0.0, 0.0, size.width, THICKNESS),
34
67
);
68
+ // OSD wyśrodkowany pod paskiem.
69
+ if osd.visible() {
70
+ let w = 340.0;
71
+ osd.paint(
72
+ &mut ctx,
73
+ Rect::from_xywh((size.width - w) / 2.0, THICKNESS + 16.0, w, 72.0),
74
+ );
75
+ }
76
+ // Powiadomienia w prawym-górnym narożniku (pod paskiem).
77
+ notes.paint(
78
+ &mut ctx,
79
+ Rect::from_xywh(0.0, THICKNESS + 8.0, size.width, size.height - THICKNESS),
80
+ );
35
81
36
82
frame.request_frame();
37
83
})
38
84
}
39
85
40
-/// Bieżąca godzina i data w UTC (bez zależności na TZ) — `("HH:MM", "DD.MM")`.
86
+/// Bieżąca godzina i data w UTC — `("HH:MM", "DD.MM")`.
41
87
fn now_clock() -> (String, String) {
42
88
let secs = SystemTime::now()
43
89
.duration_since(UNIX_EPOCH)
@@ -50,7 +96,7 @@ fn now_clock() -> (String, String) {
50
96
(format!("{h:02}:{m:02}"), format!("{d:02}.{mo:02}"))
51
97
}
52
98
53
-/// Data cywilna (rok, miesiąc, dzień) z liczby dni od 1970-01-01 (algorytm Hinnanta).
99
+/// Data cywilna z liczby dni od 1970-01-01 (algorytm Hinnanta).
54
100
fn civil_from_days(z: i64) -> (i64, u32, u32) {
55
101
let z = z + 719_468;
56
102
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
@@ -6,4 +6,6 @@
6
6
//! Moduły rysują się przez `libluna-widgets::PaintCtx`, więc ten sam kod obsługuje
7
7
//! podglądy headless (zrzuty do dokumentacji) i sesję na żywo.
8
8
9
+pub mod notifications;
10
+pub mod osd;
9
11
pub mod panel;
10
12
new file mode 100644
@@ -0,0 +1,224 @@
1
+//! Powiadomienia (toasty) w powłoce Luny — kolejka, animacja i rysowanie.
2
+//!
3
+//! To warstwa prezentacji: przyjmuje gotowe powiadomienie, animuje wejście, po
4
+//! upływie czasu samoistnie je zamyka. Podłączenie usługi D-Bus
5
+//! `org.freedesktop.Notifications` to kolejny krok (patrz roadmapa).
6
+
7
+use libluna_core::style::weight;
8
+use libluna_core::Rect;
9
+use libluna_widgets::PaintCtx;
10
+
11
+/// Czas animacji wejścia/wyjścia (s).
12
+const SLIDE: f64 = 0.22;
13
+/// Szerokość pojedynczego powiadomienia.
14
+pub const WIDTH: f64 = 340.0;
15
+/// Wysokość pojedynczego powiadomienia.
16
+pub const HEIGHT: f64 = 76.0;
17
+/// Odstęp pionowy między powiadomieniami.
18
+const GAP: f64 = 10.0;
19
+
20
+/// Pojedyncze powiadomienie.
21
+#[derive(Debug, Clone)]
22
+pub struct Notification {
23
+ /// Nazwa aplikacji (np. „Pliki”).
24
+ pub app: String,
25
+ pub title: String,
26
+ pub body: String,
27
+ /// Symbol rysowany fontem interfejsu.
28
+ pub glyph: String,
29
+ /// Czas życia (s); 0 = nie zamykaj automatycznie.
30
+ pub timeout: f64,
31
+}
32
+
33
+impl Notification {
34
+ pub fn new(app: impl Into<String>, title: impl Into<String>, body: impl Into<String>) -> Self {
35
+ Self {
36
+ app: app.into(),
37
+ title: title.into(),
38
+ body: body.into(),
39
+ glyph: "●".to_owned(),
40
+ timeout: 5.0,
41
+ }
42
+ }
43
+
44
+ pub fn with_glyph(mut self, glyph: impl Into<String>) -> Self {
45
+ self.glyph = glyph.into();
46
+ self
47
+ }
48
+}
49
+
50
+/// Kolejka powiadomień z animacją.
51
+#[derive(Default)]
52
+pub struct Notifications {
53
+ items: Vec<(Notification, f64)>,
54
+}
55
+
56
+impl Notifications {
57
+ pub fn new() -> Self {
58
+ Self::default()
59
+ }
60
+
61
+ /// Dodaje powiadomienie na wierzch stosu.
62
+ pub fn push(&mut self, n: Notification) {
63
+ self.items.push((n, 0.0));
64
+ }
65
+
66
+ /// Przesuwa czas i usuwa wygasłe powiadomienia.
67
+ pub fn advance(&mut self, dt: f64) {
68
+ for (_, age) in &mut self.items {
69
+ *age += dt;
70
+ }
71
+ self.items
72
+ .retain(|(n, age)| n.timeout <= 0.0 || *age < n.timeout);
73
+ }
74
+
75
+ pub fn len(&self) -> usize {
76
+ self.items.len()
77
+ }
78
+
79
+ pub fn is_empty(&self) -> bool {
80
+ self.items.is_empty()
81
+ }
82
+
83
+ /// Prostokąt `i`-tego powiadomienia w kolumnie zakotwiczonej w prawym-górnym
84
+ /// narożniku obszaru `area`.
85
+ pub fn rect_in(area: Rect, i: usize) -> Rect {
86
+ Rect::from_xywh(
87
+ area.right() - WIDTH,
88
+ area.y() + i as f64 * (HEIGHT + GAP),
89
+ WIDTH,
90
+ HEIGHT,
91
+ )
92
+ }
93
+
94
+ /// Rysuje stos powiadomień w obszarze `area` (od góry, do prawej).
95
+ pub fn paint(&self, ctx: &mut PaintCtx, area: Rect) {
96
+ let t = ctx.theme;
97
+ for (i, (n, age)) in self.items.iter().enumerate().rev() {
98
+ let mut rect = Self::rect_in(area, i);
99
+ // Wjazd z prawej + fade.
100
+ let p = (*age / SLIDE).clamp(0.0, 1.0);
101
+ let slide = (1.0 - p) * 28.0;
102
+ rect = Rect::from_xywh(rect.x() + slide, rect.y(), rect.width(), rect.height());
103
+ let a = p as f32;
104
+
105
+ ctx.scene
106
+ .fill_rounded_rect(rect, t.radius.medium, t.colors.card_bg.scale_alpha(a));
107
+ ctx.scene.stroke_rounded_rect(
108
+ rect,
109
+ t.radius.medium,
110
+ 1.0,
111
+ t.colors.border.scale_alpha(a),
112
+ );
113
+
114
+ let pad = t.spacing.md;
115
+ let icon = Rect::from_xywh(rect.x() + pad, rect.y() + pad, 32.0, 32.0);
116
+ ctx.scene.fill_rounded_rect(
117
+ icon,
118
+ t.radius.small,
119
+ t.colors.accent.scale_alpha(0.18 * a),
120
+ );
121
+ ctx.label_center(
122
+ icon.center().x,
123
+ icon.center().y,
124
+ &n.glyph,
125
+ 16.0,
126
+ weight::MEDIUM,
127
+ t.colors.accent.scale_alpha(a),
128
+ );
129
+
130
+ let tx = icon.right() + pad;
131
+ ctx.label_left(
132
+ tx,
133
+ rect.y() + 26.0,
134
+ &n.title,
135
+ t.typography.body.size,
136
+ weight::MEDIUM,
137
+ t.colors.text.scale_alpha(a),
138
+ );
139
+ ctx.label_left(
140
+ tx,
141
+ rect.y() + 48.0,
142
+ &n.body,
143
+ t.typography.caption.size,
144
+ weight::NORMAL,
145
+ t.colors.dim_text.scale_alpha(a),
146
+ );
147
+ ctx.label_right(
148
+ rect.right() - pad,
149
+ rect.y() + 26.0,
150
+ &n.app,
151
+ t.typography.caption.size,
152
+ weight::NORMAL,
153
+ t.colors.dim_text.scale_alpha(a),
154
+ );
155
+
156
+ // Pasek czasu (kurczy się).
157
+ if n.timeout > 0.0 {
158
+ let frac = (1.0 - (*age / n.timeout)).clamp(0.0, 1.0);
159
+ let track = Rect::from_xywh(
160
+ rect.x() + pad,
161
+ rect.bottom() - 8.0,
162
+ rect.width() - 2.0 * pad,
163
+ 3.0,
164
+ );
165
+ ctx.scene
166
+ .fill_rounded_rect(track, 1.5, t.colors.shade.scale_alpha(a));
167
+ let fill =
168
+ Rect::from_xywh(track.x(), track.y(), track.width() * frac, track.height());
169
+ ctx.scene
170
+ .fill_rounded_rect(fill, 1.5, t.colors.accent.scale_alpha(a));
171
+ }
172
+ }
173
+ }
174
+}
175
+
176
+#[cfg(test)]
177
+mod tests {
178
+ use super::*;
179
+ use libluna_core::Theme;
180
+ use libluna_render::{Scene, TextEngine};
181
+
182
+ #[test]
183
+ fn expires_after_timeout() {
184
+ let mut n = Notifications::new();
185
+ n.push(Notification::new("Pliki", "Kopiowanie", "12 plików"));
186
+ assert_eq!(n.len(), 1);
187
+ n.advance(3.0);
188
+ assert_eq!(n.len(), 1);
189
+ n.advance(3.0);
190
+ assert!(n.is_empty());
191
+ }
192
+
193
+ #[test]
194
+ fn zero_timeout_is_sticky() {
195
+ let mut n = Notifications::new();
196
+ let mut item = Notification::new("System", "Aktualizacja", "W toku");
197
+ item.timeout = 0.0;
198
+ n.push(item);
199
+ n.advance(100.0);
200
+ assert_eq!(n.len(), 1);
201
+ }
202
+
203
+ #[test]
204
+ fn rects_stack_downward_from_top_right() {
205
+ let area = Rect::from_xywh(0.0, 40.0, 1280.0, 200.0);
206
+ let a = Notifications::rect_in(area, 0);
207
+ let b = Notifications::rect_in(area, 1);
208
+ assert_eq!(a.right(), area.right());
209
+ assert!(b.y() > a.y());
210
+ }
211
+
212
+ #[test]
213
+ fn paints_without_window() {
214
+ let theme = Theme::dark();
215
+ let text = TextEngine::none();
216
+ let mut scene = Scene::new();
217
+ let mut ctx = PaintCtx::new(&mut scene, &theme, &text);
218
+ let mut n = Notifications::new();
219
+ n.push(Notification::new("Pliki", "Kopiowanie", "12 plików"));
220
+ n.advance(0.3);
221
+ n.paint(&mut ctx, Rect::from_xywh(0.0, 40.0, 1280.0, 200.0));
222
+ assert!(scene.len() > 5);
223
+ }
224
+}
0
225
new file mode 100644
@@ -0,0 +1,157 @@
1
+//! OSD — ekranowe wskaźniki (głośność, jasność, …) pojawiające się na chwilę.
2
+//!
3
+//! Model jest czysty (bez okna), więc łatwo go testować i podglądać headless.
4
+
5
+use libluna_core::style::weight;
6
+use libluna_core::Rect;
7
+use libluna_widgets::PaintCtx;
8
+
9
+/// Czas wchodzenia (s).
10
+const FADE_IN: f64 = 0.18;
11
+/// Czas wychodzenia (s).
12
+const FADE_OUT: f64 = 0.45;
13
+
14
+/// Wskaźnik OSD.
15
+#[derive(Debug, Clone)]
16
+pub struct Osd {
17
+ /// Symbol rysowany fontem interfejsu.
18
+ pub glyph: String,
19
+ /// Etykieta (np. „Głośność”).
20
+ pub label: String,
21
+ /// Wartość 0..1 (poziom).
22
+ pub value: f64,
23
+ age: f64,
24
+ lifetime: f64,
25
+}
26
+
27
+impl Osd {
28
+ /// Tworzy wskaźnik i włącza jego wyświetlanie.
29
+ pub fn new(glyph: impl Into<String>, label: impl Into<String>, value: f64) -> Self {
30
+ Self {
31
+ glyph: glyph.into(),
32
+ label: label.into(),
33
+ value: value.clamp(0.0, 1.0),
34
+ age: 0.0,
35
+ lifetime: 2.0,
36
+ }
37
+ }
38
+
39
+ /// Pokazuje wskaźnik ponownie z nową wartością (reset licznika).
40
+ pub fn show(&mut self, label: impl Into<String>, value: f64) {
41
+ self.label = label.into();
42
+ self.value = value.clamp(0.0, 1.0);
43
+ self.age = 0.0;
44
+ }
45
+
46
+ /// Przesuwa czas animacji.
47
+ pub fn advance(&mut self, dt: f64) {
48
+ self.age += dt;
49
+ }
50
+
51
+ /// Czy wskaźnik jest jeszcze widoczny.
52
+ pub fn visible(&self) -> bool {
53
+ self.age < self.lifetime
54
+ }
55
+
56
+ /// Przezroczystość (0..1) wynikająca z animacji wejścia/wyjścia.
57
+ pub fn opacity(&self) -> f64 {
58
+ let fade_in = (self.age / FADE_IN).clamp(0.0, 1.0);
59
+ let fade_out = ((self.lifetime - self.age) / FADE_OUT).clamp(0.0, 1.0);
60
+ fade_in.min(fade_out)
61
+ }
62
+
63
+ /// Rysuje wskaźnik w zadanym prostokącie.
64
+ pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
65
+ let a = self.opacity() as f32;
66
+ if a <= 0.01 {
67
+ return;
68
+ }
69
+ let t = ctx.theme;
70
+ let cy = rect.center().y;
71
+ let pad = t.spacing.lg;
72
+
73
+ // Tło „pigułki” + obramowanie.
74
+ ctx.scene
75
+ .fill_rounded_rect(rect, t.radius.medium, t.colors.card_bg.scale_alpha(a));
76
+ ctx.scene
77
+ .stroke_rounded_rect(rect, t.radius.medium, 1.0, t.colors.border.scale_alpha(a));
78
+
79
+ // Ikona w kwadracie akcentu.
80
+ let icon = Rect::from_xywh(rect.x() + pad, cy - 20.0, 40.0, 40.0);
81
+ ctx.scene
82
+ .fill_rounded_rect(icon, t.radius.small, t.colors.accent.scale_alpha(0.18 * a));
83
+ ctx.label_center(
84
+ icon.center().x,
85
+ icon.center().y,
86
+ &self.glyph,
87
+ 20.0,
88
+ weight::MEDIUM,
89
+ t.colors.accent.scale_alpha(a),
90
+ );
91
+
92
+ // Etykieta i wartość procentowa.
93
+ let text_x = icon.right() + t.spacing.md;
94
+ ctx.label_left(
95
+ text_x,
96
+ rect.y() + 24.0,
97
+ &self.label,
98
+ t.typography.body.size,
99
+ weight::MEDIUM,
100
+ t.colors.text.scale_alpha(a),
101
+ );
102
+ ctx.label_right(
103
+ rect.right() - pad,
104
+ rect.y() + 24.0,
105
+ &format!("{}%", (self.value * 100.0).round() as i32),
106
+ t.typography.body.size,
107
+ weight::NORMAL,
108
+ t.colors.dim_text.scale_alpha(a),
109
+ );
110
+
111
+ // Pasek poziomu.
112
+ let bar = Rect::from_xywh(
113
+ text_x,
114
+ rect.bottom() - 22.0,
115
+ (rect.right() - pad) - text_x,
116
+ 6.0,
117
+ );
118
+ ctx.scene
119
+ .fill_rounded_rect(bar, 3.0, t.colors.shade.scale_alpha(a));
120
+ if self.value > 0.0 {
121
+ let fill = Rect::from_xywh(bar.x(), bar.y(), bar.width() * self.value, bar.height());
122
+ ctx.scene
123
+ .fill_rounded_rect(fill, 3.0, t.colors.accent.scale_alpha(a));
124
+ }
125
+ }
126
+}
127
+
128
+#[cfg(test)]
129
+mod tests {
130
+ use super::*;
131
+ use libluna_core::Theme;
132
+ use libluna_render::{Scene, TextEngine};
133
+
134
+ #[test]
135
+ fn fades_in_and_out() {
136
+ let mut osd = Osd::new("◆", "Głośność", 0.5);
137
+ assert!(osd.visible());
138
+ assert!(osd.opacity() < 0.05); // dopiero start
139
+ osd.advance(0.3);
140
+ assert!(osd.opacity() > 0.9);
141
+ osd.advance(2.0);
142
+ assert!(!osd.visible());
143
+ assert!(osd.opacity() < 0.05);
144
+ }
145
+
146
+ #[test]
147
+ fn paints_without_window() {
148
+ let theme = Theme::dark();
149
+ let text = TextEngine::none();
150
+ let mut scene = Scene::new();
151
+ let mut ctx = PaintCtx::new(&mut scene, &theme, &text);
152
+ let mut osd = Osd::new("◆", "Głośność", 0.65);
153
+ osd.advance(0.3);
154
+ osd.paint(&mut ctx, Rect::from_xywh(0.0, 0.0, 320.0, 72.0));
155
+ assert!(scene.len() > 5);
156
+ }
157
+}
0
158
Binary files a/docs/images/luna-shell-dark.png and b/docs/images/luna-shell-dark.png differ
1
159
Binary files a/docs/images/luna-shell-light.png and b/docs/images/luna-shell-light.png differ
@@ -166,7 +166,11 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
166
166
Dane (`PanelState`, `App`) oddzielone od rysowania (`PaintCtx`) — ten sam kod do podglądów
167
167
i sesji na żywo. ✅ **`luna-shell`** (bin) uruchamia panel jako prawdziwą **powierzchnię
168
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
+ kończy się czytelnym `NoCompositor`. ✅ **OSD** (`osd.rs`: głośność/jasność, animacja
170
+ wejścia/wyjścia) i **powiadomienia** (`notifications.rs`: kolejka, wjazd z prawej, pasek
171
+ czasu, auto-zamknięcie) — pokazywane pod paskiem na powierzchni „chrome”; podglądy
172
+ w `docs/images/luna-shell-{light,dark}.png`. Dalej: usługa D-Bus
173
+ `org.freedesktop.Notifications`, menu kontekstowe, centrum powiadomień.
170
174
171
175
## Później (poza MVP)
172
176
- Dostępność AT-SPI, IME, schowek, drag & drop, animacje, backend GPU, mini-CSS dla motywów.