← luna

Commit 53ad27f

4
files
+262
added
-8
deleted
@@ -43,6 +43,9 @@ fn main() -> Result<(), Box<dyn Error>> {
43 43 };
44 44 let mut launcher = Launcher::new();
45 45 let mut open = false;
46 + // Menu zasilania (wyloguj/restart/wyłącz).
47 + let power_items = luna::power::items();
48 + let mut power_open = false;
46 49 let mut osd = Osd::new(IconName::Volume, "Głośność", 0.0);
47 50 let mut notes = Notifications::new();
48 51 // Lista okien z kompozytora (foreign-toplevel) — własne połączenie + wątek.
@@ -50,7 +53,7 @@ fn main() -> Result<(), Box<dyn Error>> {
50 53 // Realny stan systemu (użytkownik, dźwięk, Wi-Fi, bateria) — odpytywany w tle.
51 54 let system = luna::system::spawn();
52 55 // Sygnatura stanu — przerysowujemy tylko, gdy coś się zmieniło.
53 - let mut last_sig: (bool, String, u64) = (false, String::new(), u64::MAX);
56 + let mut last_sig: (bool, bool, String, u64) = (false, false, String::new(), u64::MAX);
54 57
55 58 run(cfg, move |frame: &mut Frame| {
56 59 let dt = frame.delta_time();
@@ -85,7 +88,18 @@ fn main() -> Result<(), Box<dyn Error>> {
85 88 Event::PointerButton(pe)
86 89 if pe.state == ButtonState::Pressed && pe.button == PointerButton::Left =>
87 90 {
88 - if panel::burger_rect(bar, &frame.theme).contains(pe.position) {
91 + if panel::power_rect(bar, &frame.theme).contains(pe.position) {
92 + // Przycisk zasilania — otwórz/zamknij menu.
93 + power_open = !power_open;
94 + } else if power_open {
95 + // Menu zasilania otwarte: wybierz pozycję albo zamknij.
96 + if let Some(i) =
97 + panel::power_item_at(bar, &frame.theme, power_items.len(), pe.position)
98 + {
99 + luna::power::perform(power_items[i].action);
100 + }
101 + power_open = false;
102 + } else if panel::burger_rect(bar, &frame.theme).contains(pe.position) {
89 103 open = !open;
90 104 if open {
91 105 let max = panel::launcher_scroll_max(
@@ -206,6 +220,10 @@ fn main() -> Result<(), Box<dyn Error>> {
206 220 if open {
207 221 panel::paint_launcher(&apps, &launcher, &mut ctx, larea);
208 222 }
223 + if power_open {
224 + let labels: Vec<&str> = power_items.iter().map(|p| p.label.as_str()).collect();
225 + panel::paint_power_menu(&mut ctx, bar, &labels);
226 + }
209 227 if osd.visible() {
210 228 let w = 340.0;
211 229 osd.paint(
@@ -234,7 +252,7 @@ fn main() -> Result<(), Box<dyn Error>> {
234 252 th = th
235 253 .wrapping_mul(1_099_511_628_211)
236 254 .wrapping_add(state.user.len() as u64);
237 - let sig = (open, state.clock.clone(), th);
255 + let sig = (open, power_open, state.clock.clone(), th);
238 256 let animating = osd.visible() || notes.visible();
239 257 if !frame.events.is_empty() || animating || open || sig != last_sig {
240 258 frame.request_frame();
@@ -10,5 +10,6 @@ pub mod apps;
10 10 pub mod notifications;
11 11 pub mod osd;
12 12 pub mod panel;
13 +pub mod power;
13 14 pub mod system;
14 15 pub mod toplevels;
@@ -19,6 +19,12 @@ pub const THICKNESS: f64 = 40.0;
19 19 pub const LAUNCHER_W: f64 = 720.0;
20 20 /// Domyślna wysokość karty launchera.
21 21 pub const LAUNCHER_H: f64 = 600.0;
22 +/// Bok przycisku zasilania w pasku.
23 +pub const POWER_BTN: f64 = 26.0;
24 +/// Szerokość rozwijanego menu zasilania.
25 +pub const POWER_MENU_W: f64 = 176.0;
26 +/// Wysokość pozycji menu zasilania.
27 +pub const POWER_ITEM_H: f64 = 34.0;
22 28
23 29 const GRID_COLS: usize = 4;
24 30 const GRID_CELL_H: f64 = 76.0;
@@ -267,6 +273,18 @@ pub fn burger_rect(panel: Rect, theme: &Theme) -> Rect {
267 273 Rect::from_xywh(panel.x() + theme.spacing.md, cy - 14.0, 28.0, 28.0)
268 274 }
269 275
276 +/// Prostokąt przycisku zasilania (skrajna prawa strona paska).
277 +pub fn power_rect(bar: Rect, theme: &Theme) -> Rect {
278 + let cy = bar.center().y;
279 + let pad = theme.spacing.md;
280 + Rect::from_xywh(
281 + bar.right() - pad - POWER_BTN,
282 + cy - POWER_BTN / 2.0,
283 + POWER_BTN,
284 + POWER_BTN,
285 + )
286 +}
287 +
270 288 /// Prostokąty pigułek przełączników (spójne z `paint_panel`), w kolejności `state.toggles`.
271 289 pub fn toggle_rects(bar: Rect, theme: &Theme, text: &TextEngine, state: &PanelState) -> Vec<Rect> {
272 290 let t = theme;
@@ -276,8 +294,8 @@ pub fn toggle_rects(bar: Rect, theme: &Theme, text: &TextEngine, state: &PanelSt
276 294 let uw = text
277 295 .measure(&state.user, t.typography.body.size, weight::NORMAL)
278 296 .width;
279 - // Za użytkownikiem i separatorem (jak w `paint_panel`).
280 - let mut right = bar.right() - pad - uw - 2.0 * t.spacing.md;
297 + // Za użytkownikiem i separatorem (jak w `paint_panel`); dalej przycisk zasilania.
298 + let mut right = bar.right() - pad - POWER_BTN - t.spacing.sm - uw - 2.0 * t.spacing.md;
281 299 let mut rects = vec![Rect::ZERO; state.toggles.len()];
282 300 for (i, toggle) in state.toggles.iter().enumerate().rev() {
283 301 let icon = 13.0;
@@ -303,6 +321,59 @@ pub fn toggle_at(
303 321 .position(|r| r.contains(p))
304 322 }
305 323
324 +/// Prostokąt rozwijanego menu zasilania (pod przyciskiem, wyrównany do prawej).
325 +pub fn power_menu_rect(bar: Rect, theme: &Theme, items: usize) -> Rect {
326 + let p = power_rect(bar, theme);
327 + let pad = 6.0;
328 + let h = items as f64 * POWER_ITEM_H + 2.0 * pad;
329 + Rect::from_xywh(
330 + p.right() - POWER_MENU_W,
331 + bar.bottom() + pad,
332 + POWER_MENU_W,
333 + h,
334 + )
335 +}
336 +
337 +/// Indeks pozycji menu zasilania pod punktem `p`.
338 +pub fn power_item_at(bar: Rect, theme: &Theme, items: usize, p: Point) -> Option<usize> {
339 + let menu = power_menu_rect(bar, theme, items);
340 + let inner = Rect::from_xywh(
341 + menu.x() + 6.0,
342 + menu.y() + 6.0,
343 + menu.width() - 12.0,
344 + items as f64 * POWER_ITEM_H,
345 + );
346 + if !inner.contains(p) {
347 + return None;
348 + }
349 + let i = ((p.y - inner.y()) / POWER_ITEM_H).floor() as usize;
350 + (i < items).then_some(i)
351 +}
352 +
353 +/// Rysuje menu zasilania z podanymi etykietami (spójne z [`power_item_at`]).
354 +pub fn paint_power_menu(ctx: &mut PaintCtx, bar: Rect, labels: &[&str]) {
355 + let t = ctx.theme;
356 + let menu = power_menu_rect(bar, t, labels.len());
357 + ctx.scene
358 + .fill_rounded_rect(menu, t.radius.medium, t.colors.card_bg);
359 + ctx.scene
360 + .stroke_rounded_rect(menu, t.radius.medium, 1.0, t.colors.border);
361 + let x = menu.x() + 6.0;
362 + let y = menu.y() + 6.0;
363 + let w = menu.width() - 12.0;
364 + for (i, label) in labels.iter().enumerate() {
365 + let row = Rect::from_xywh(x, y + i as f64 * POWER_ITEM_H, w, POWER_ITEM_H);
366 + ctx.label_left(
367 + row.x() + 10.0,
368 + row.center().y,
369 + label,
370 + t.typography.body.size,
371 + weight::NORMAL,
372 + t.colors.text,
373 + );
374 + }
375 +}
376 +
306 377 /// Rozmieszczenie przycisków okien (taskbar) w pasku.
307 378 pub fn task_rects(bar: Rect, theme: &Theme, text: &TextEngine, tasks: &[Task]) -> Vec<Rect> {
308 379 let t = theme;
@@ -468,9 +539,24 @@ pub fn paint_panel(panel: &PanelState, ctx: &mut PaintCtx, rect: Rect) {
468 539 t.colors.text,
469 540 );
470 541
471 - // ── Prawa grupa (od prawej): użytkownik, przełączniki, przestrzenie ──
472 - let pad = t.spacing.md;
473 - let mut right = rect.right() - pad;
542 + // ── Prawa grupa (od prawej): zasilanie, użytkownik, przełączniki, przestrzenie ──
543 + // Przycisk zasilania (skrajna prawa strona).
544 + let power = power_rect(rect, t);
545 + ctx.scene
546 + .fill_rounded_rect(power, t.radius.small, t.colors.shade);
547 + let pisz = POWER_BTN * 0.6;
548 + paint_icon(
549 + ctx,
550 + IconName::Power,
551 + Rect::from_xywh(
552 + power.center().x - pisz / 2.0,
553 + power.center().y - pisz / 2.0,
554 + pisz,
555 + pisz,
556 + ),
557 + t.colors.text,
558 + );
559 + let mut right = power.x() - t.spacing.sm;
474 560 let uw = ctx.measure(&panel.user, body, weight::NORMAL);
475 561 ctx.label_right(right, cy, &panel.user, body, weight::NORMAL, t.colors.text);
476 562 right -= uw + t.spacing.md;
@@ -1011,6 +1097,34 @@ mod tests {
1011 1097 Rect::from_xywh(100.0, 60.0, LAUNCHER_W, LAUNCHER_H)
1012 1098 }
1013 1099
1100 + #[test]
1101 + fn power_menu_items_hit_test() {
1102 + let theme = Theme::dark();
1103 + let bar = Rect::from_xywh(0.0, 0.0, 1280.0, THICKNESS);
1104 + let n = 3;
1105 + let menu = power_menu_rect(bar, &theme, n);
1106 + let row = |i: usize| {
1107 + Rect::from_xywh(
1108 + menu.x() + 6.0,
1109 + menu.y() + 6.0 + i as f64 * POWER_ITEM_H,
1110 + menu.width() - 12.0,
1111 + POWER_ITEM_H,
1112 + )
1113 + };
1114 + for i in 0..n {
1115 + assert_eq!(power_item_at(bar, &theme, n, row(i).center()), Some(i));
1116 + }
1117 + // Poza pozycjami (róg menu) brak trafienia.
1118 + assert_eq!(
1119 + power_item_at(bar, &theme, n, Point::new(menu.x() + 1.0, menu.y() + 1.0)),
1120 + None
1121 + );
1122 + // Przycisk zasilania mieści się w pasku, przy prawej krawędzi.
1123 + let p = power_rect(bar, &theme);
1124 + assert!(p.right() <= bar.right());
1125 + assert!(bar.contains(p.center()));
1126 + }
1127 +
1014 1128 fn panel_commands(view: LauncherView) -> usize {
1015 1129 let theme = Theme::light();
1016 1130 let text = TextEngine::none();
1017 1131 new file mode 100644
@@ -0,0 +1,121 @@
1 +//! Menu zasilania powłoki: wylogowanie, restart, wyłączenie.
2 +//!
3 +//! Akcje idą przez `loginctl` (logind): sesja jest kończona po identyfikatorze
4 +//! sesji, a restart/wyłączenie to `loginctl reboot` / `loginctl poweroff`. Dzięki
5 +//! temu całość działa w zwykłej sesji użytkownika (polkit zezwala sesji aktywnej).
6 +
7 +use std::process::Command;
8 +
9 +/// Akcja pozycji menu zasilania.
10 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11 +pub enum PowerAction {
12 + /// Zakończ bieżącą sesję.
13 + Logout,
14 + /// Uruchom ponownie komputer.
15 + Reboot,
16 + /// Wyłącz komputer.
17 + PowerOff,
18 +}
19 +
20 +/// Pojedyncza pozycja menu.
21 +#[derive(Debug, Clone)]
22 +pub struct PowerItem {
23 + pub label: String,
24 + pub action: PowerAction,
25 +}
26 +
27 +/// Pozycje menu (z lokalizacją).
28 +pub fn items() -> Vec<PowerItem> {
29 + vec![
30 + PowerItem {
31 + label: libluna_i18n::tr("Wyloguj").into(),
32 + action: PowerAction::Logout,
33 + },
34 + PowerItem {
35 + label: libluna_i18n::tr("Restart").into(),
36 + action: PowerAction::Reboot,
37 + },
38 + PowerItem {
39 + label: libluna_i18n::tr("Wyłącz").into(),
40 + action: PowerAction::PowerOff,
41 + },
42 + ]
43 +}
44 +
45 +/// Wykonuje akcję (`loginctl` w tle; powłoka zaraz zniknie).
46 +pub fn perform(action: PowerAction) {
47 + match action {
48 + PowerAction::Logout => {
49 + if let Some(args) = logout_args(
50 + std::env::var("XDG_SESSION_ID").ok().as_deref(),
51 + std::env::var("USER").ok().as_deref(),
52 + ) {
53 + spawn("loginctl", &args);
54 + }
55 + }
56 + PowerAction::Reboot => spawn("loginctl", &["reboot".to_owned()]),
57 + PowerAction::PowerOff => spawn("loginctl", &["poweroff".to_owned()]),
58 + }
59 +}
60 +
61 +/// Argumenty `loginctl` do wylogowania: najpierw sesja, w ostateczności cały user.
62 +///
63 +/// Rozdzielone od [`perform`], żeby dało się to przetestować bez uruchamiania czegokolwiek.
64 +fn logout_args(session_id: Option<&str>, user: Option<&str>) -> Option<Vec<String>> {
65 + match (
66 + session_id.filter(|s| !s.is_empty()),
67 + user.filter(|u| !u.is_empty()),
68 + ) {
69 + (Some(id), _) => Some(vec!["terminate-session".to_owned(), id.to_owned()]),
70 + (None, Some(u)) => Some(vec!["terminate-user".to_owned(), u.to_owned()]),
71 + _ => None,
72 + }
73 +}
74 +
75 +fn spawn(program: &str, args: &[String]) {
76 + let _ = Command::new(program).args(args).spawn();
77 +}
78 +
79 +#[cfg(test)]
80 +mod tests {
81 + use super::*;
82 +
83 + #[test]
84 + fn logout_prefers_the_session() {
85 + assert_eq!(
86 + logout_args(Some("c3"), Some("patryks")),
87 + Some(vec!["terminate-session".into(), "c3".into()])
88 + );
89 + }
90 +
91 + #[test]
92 + fn logout_falls_back_to_user() {
93 + assert_eq!(
94 + logout_args(None, Some("patryks")),
95 + Some(vec!["terminate-user".into(), "patryks".into()])
96 + );
97 + assert_eq!(
98 + logout_args(Some(""), Some("patryks")),
99 + Some(vec!["terminate-user".into(), "patryks".into()])
100 + );
101 + }
102 +
103 + #[test]
104 + fn logout_without_any_target_is_a_noop() {
105 + assert_eq!(logout_args(None, None), None);
106 + assert_eq!(logout_args(Some(""), None), None);
107 + }
108 +
109 + #[test]
110 + fn menu_has_logout_reboot_poweroff() {
111 + let actions: Vec<_> = items().iter().map(|i| i.action).collect();
112 + assert_eq!(
113 + actions,
114 + vec![
115 + PowerAction::Logout,
116 + PowerAction::Reboot,
117 + PowerAction::PowerOff
118 + ]
119 + );
120 + }
121 +}