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
+}