Commit 45eef39
9
files
+311
added
-84
deleted
@@ -254,6 +254,74 @@ pub enum IconName {
254
254
App,
255
255
}
256
256
257
+impl IconName {
258
+ /// Wszystkie ikony z pakietu (kolejność = kolejność w galerii).
259
+ pub const ALL: [IconName; 28] = [
260
+ IconName::Menu,
261
+ IconName::Search,
262
+ IconName::Files,
263
+ IconName::Settings,
264
+ IconName::Terminal,
265
+ IconName::Music,
266
+ IconName::Star,
267
+ IconName::Clock,
268
+ IconName::Store,
269
+ IconName::Edit,
270
+ IconName::Wifi,
271
+ IconName::Volume,
272
+ IconName::Battery,
273
+ IconName::Power,
274
+ IconName::Grid,
275
+ IconName::List,
276
+ IconName::Carousel,
277
+ IconName::ChevronLeft,
278
+ IconName::ChevronRight,
279
+ IconName::Close,
280
+ IconName::Minimize,
281
+ IconName::Maximize,
282
+ IconName::Check,
283
+ IconName::Plus,
284
+ IconName::Home,
285
+ IconName::User,
286
+ IconName::Info,
287
+ IconName::App,
288
+ ];
289
+
290
+ /// Nazwa ikony (do galerii/diagnostyki).
291
+ pub fn name(self) -> &'static str {
292
+ match self {
293
+ IconName::Menu => "Menu",
294
+ IconName::Search => "Search",
295
+ IconName::Files => "Files",
296
+ IconName::Settings => "Settings",
297
+ IconName::Terminal => "Terminal",
298
+ IconName::Music => "Music",
299
+ IconName::Star => "Star",
300
+ IconName::Clock => "Clock",
301
+ IconName::Store => "Store",
302
+ IconName::Edit => "Edit",
303
+ IconName::Wifi => "Wifi",
304
+ IconName::Volume => "Volume",
305
+ IconName::Battery => "Battery",
306
+ IconName::Power => "Power",
307
+ IconName::Grid => "Grid",
308
+ IconName::List => "List",
309
+ IconName::Carousel => "Carousel",
310
+ IconName::ChevronLeft => "ChevronLeft",
311
+ IconName::ChevronRight => "ChevronRight",
312
+ IconName::Close => "Close",
313
+ IconName::Minimize => "Minimize",
314
+ IconName::Maximize => "Maximize",
315
+ IconName::Check => "Check",
316
+ IconName::Plus => "Plus",
317
+ IconName::Home => "Home",
318
+ IconName::User => "User",
319
+ IconName::Info => "Info",
320
+ IconName::App => "App",
321
+ }
322
+ }
323
+}
324
+
257
325
/// Współrzędne w siatce 24×24 mapowane na prostokąt docelowy.
258
326
struct Grid {
259
327
ox: f64,
@@ -579,36 +647,7 @@ mod icon_tests {
579
647
fn every_icon_draws_without_panic() {
580
648
let theme = Theme::dark();
581
649
let text = TextEngine::none();
582
- for name in [
583
- IconName::Menu,
584
- IconName::Search,
585
- IconName::Files,
586
- IconName::Settings,
587
- IconName::Terminal,
588
- IconName::Music,
589
- IconName::Star,
590
- IconName::Clock,
591
- IconName::Store,
592
- IconName::Edit,
593
- IconName::Wifi,
594
- IconName::Volume,
595
- IconName::Battery,
596
- IconName::Power,
597
- IconName::Grid,
598
- IconName::List,
599
- IconName::Carousel,
600
- IconName::ChevronLeft,
601
- IconName::ChevronRight,
602
- IconName::Close,
603
- IconName::Minimize,
604
- IconName::Maximize,
605
- IconName::Check,
606
- IconName::Plus,
607
- IconName::Home,
608
- IconName::User,
609
- IconName::Info,
610
- IconName::App,
611
- ] {
650
+ for name in IconName::ALL {
612
651
let mut scene = Scene::new();
613
652
let mut ctx = PaintCtx::new(&mut scene, &theme, &text);
614
653
paint_icon(
@@ -8,7 +8,7 @@ use std::error::Error;
8
8
9
9
use libluna_core::{Rect, Size, Theme};
10
10
use libluna_render::{Scene, SoftwareRenderer, TextEngine};
11
-use libluna_widgets::PaintCtx;
11
+use libluna_widgets::{IconName, PaintCtx};
12
12
use luna::notifications::{Notification, Notifications};
13
13
use luna::osd::Osd;
14
14
use luna::panel::{self, App, Launcher, LauncherView, PanelState};
@@ -29,11 +29,12 @@ fn main() -> Result<(), Box<dyn Error>> {
29
29
let launcher = Launcher {
30
30
view: LauncherView::Grid,
31
31
carousel: 0,
32
+ selected: None,
32
33
scroll: 0.0,
33
34
};
34
35
panel::paint_desktop(&pstate, &apps, &launcher, &mut paint, size);
35
36
36
- let mut osd = Osd::new("◆", "Głośność", 0.65);
37
+ let mut osd = Osd::new(IconName::Volume, "Głośność", 0.65);
37
38
osd.advance(0.3);
38
39
osd.paint(
39
40
&mut paint,
@@ -43,9 +44,11 @@ fn main() -> Result<(), Box<dyn Error>> {
43
44
let mut notes = Notifications::new();
44
45
notes.push(
45
46
Notification::new("Pliki", "Kopiowanie zakończone", "12 plików → Dokumenty")
46
- .with_glyph("◆"),
47
+ .with_icon(IconName::Files),
48
+ );
49
+ notes.push(
50
+ Notification::new("System", "Bateria", "Pozostało 18%").with_icon(IconName::Battery),
47
51
);
48
- notes.push(Notification::new("System", "Bateria", "Pozostało 18%").with_glyph("■"));
49
52
notes.advance(0.3);
50
53
notes.paint(&mut paint, Rect::from_xywh(0.0, 56.0, W as f64, 240.0));
51
54
@@ -68,6 +71,7 @@ fn main() -> Result<(), Box<dyn Error>> {
68
71
let launcher = Launcher {
69
72
view,
70
73
carousel: 2,
74
+ selected: None,
71
75
scroll: if view == LauncherView::List {
72
76
140.0
73
77
} else {
@@ -11,7 +11,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
11
11
12
12
use libluna_core::{ButtonState, Event, PointerButton, Rect};
13
13
use libluna_platform::{run, AppConfig, Frame, LayerConfig};
14
-use libluna_widgets::PaintCtx;
14
+use libluna_widgets::{IconName, PaintCtx};
15
15
use luna::notifications::{Notification, Notifications};
16
16
use luna::osd::Osd;
17
17
use luna::panel::{self, App, Launcher, PanelState, LAUNCHER_H, LAUNCHER_W, THICKNESS};
@@ -39,7 +39,7 @@ fn main() -> Result<(), Box<dyn Error>> {
39
39
};
40
40
let mut launcher = Launcher::new();
41
41
let mut open = false;
42
- let mut osd = Osd::new("◆", "Głośność", 0.0);
42
+ let mut osd = Osd::new(IconName::Volume, "Głośność", 0.0);
43
43
let mut notes = Notifications::new();
44
44
let mut timer = 0.0f64;
45
45
@@ -88,10 +88,7 @@ fn main() -> Result<(), Box<dyn Error>> {
88
88
pe.position,
89
89
) {
90
90
// Uruchom aplikację (jeśli ma polecenie).
91
- if let Some(exec) = apps.get(i).and_then(|a| a.exec.clone()) {
92
- let _ =
93
- std::process::Command::new("sh").arg("-c").arg(exec).spawn();
94
- }
91
+ launch(&apps, i);
95
92
open = false;
96
93
}
97
94
}
@@ -106,7 +103,41 @@ fn main() -> Result<(), Box<dyn Error>> {
106
103
launcher.scroll = (launcher.scroll - se.dy * 24.0).clamp(0.0, max);
107
104
}
108
105
}
109
- Event::Key(ke) if ke.keysym == 0xff1b => open = false, // Escape
106
+ Event::Key(ke) if ke.state == ButtonState::Pressed && open => match ke.keysym {
107
+ 0xff1b => open = false, // Escape
108
+ // Góra/dół: pionowa nawigacja.
109
+ 0xff52 | 0xff54 => {
110
+ let dy = if ke.keysym == 0xff52 { -1 } else { 1 };
111
+ launcher.navigate(apps.len(), 0, dy);
112
+ panel::ensure_selection_visible(
113
+ &apps,
114
+ &mut launcher,
115
+ larea,
116
+ &frame.theme,
117
+ text,
118
+ );
119
+ }
120
+ // Lewo/prawo: pozioma nawigacja (kolumna w siatce, karta w karuzeli).
121
+ 0xff51 | 0xff53 => {
122
+ let dx = if ke.keysym == 0xff51 { -1 } else { 1 };
123
+ launcher.navigate(apps.len(), dx, 0);
124
+ panel::ensure_selection_visible(
125
+ &apps,
126
+ &mut launcher,
127
+ larea,
128
+ &frame.theme,
129
+ text,
130
+ );
131
+ }
132
+ // Enter / Enter (numeryczny): uruchom zaznaczoną pozycję.
133
+ 0xff0d | 0xff8d => {
134
+ if let Some(i) = launcher.selected_index(apps.len()) {
135
+ launch(&apps, i);
136
+ open = false;
137
+ }
138
+ }
139
+ _ => {}
140
+ },
110
141
_ => {}
111
142
}
112
143
}
@@ -124,7 +155,7 @@ fn main() -> Result<(), Box<dyn Error>> {
124
155
if timer % 4.0 < dt {
125
156
notes.push(
126
157
Notification::new("Pliki", "Kopiowanie zakończone", "12 plików → Dokumenty")
127
- .with_glyph("◆"),
158
+ .with_icon(IconName::Files),
128
159
);
129
160
}
130
161
osd.advance(dt);
@@ -152,6 +183,13 @@ fn main() -> Result<(), Box<dyn Error>> {
152
183
})
153
184
}
154
185
186
+/// Uruchamia aplikację o indeksie `i` (jeśli ma polecenie).
187
+fn launch(apps: &[App], i: usize) {
188
+ if let Some(exec) = apps.get(i).and_then(|a| a.exec.clone()) {
189
+ let _ = std::process::Command::new("sh").arg("-c").arg(exec).spawn();
190
+ }
191
+}
192
+
155
193
/// Bieżąca godzina i data w UTC — `("HH:MM", "DD.MM")`.
156
194
fn now_clock() -> (String, String) {
157
195
let secs = SystemTime::now()
@@ -6,7 +6,7 @@
6
6
7
7
use libluna_core::style::weight;
8
8
use libluna_core::Rect;
9
-use libluna_widgets::PaintCtx;
9
+use libluna_widgets::{paint_icon, IconName, PaintCtx};
10
10
11
11
/// Czas animacji wejścia/wyjścia (s).
12
12
const SLIDE: f64 = 0.22;
@@ -24,8 +24,8 @@ pub struct Notification {
24
24
pub app: String,
25
25
pub title: String,
26
26
pub body: String,
27
- /// Symbol rysowany fontem interfejsu.
28
- pub glyph: String,
27
+ /// Ikona z pakietu Luny.
28
+ pub icon: IconName,
29
29
/// Czas życia (s); 0 = nie zamykaj automatycznie.
30
30
pub timeout: f64,
31
31
}
@@ -36,13 +36,13 @@ impl Notification {
36
36
app: app.into(),
37
37
title: title.into(),
38
38
body: body.into(),
39
- glyph: "●".to_owned(),
39
+ icon: IconName::Info,
40
40
timeout: 5.0,
41
41
}
42
42
}
43
43
44
- pub fn with_glyph(mut self, glyph: impl Into<String>) -> Self {
45
- self.glyph = glyph.into();
44
+ pub fn with_icon(mut self, icon: IconName) -> Self {
45
+ self.icon = icon;
46
46
self
47
47
}
48
48
}
@@ -118,14 +118,8 @@ impl Notifications {
118
118
t.radius.small,
119
119
t.colors.accent.scale_alpha(0.18 * a),
120
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
- );
121
+ let inner = Rect::from_xywh(icon.x() + 7.0, icon.y() + 7.0, 18.0, 18.0);
122
+ paint_icon(ctx, n.icon, inner, t.colors.accent.scale_alpha(a));
129
123
130
124
let tx = icon.right() + pad;
131
125
ctx.label_left(
@@ -216,7 +210,7 @@ mod tests {
216
210
let mut scene = Scene::new();
217
211
let mut ctx = PaintCtx::new(&mut scene, &theme, &text);
218
212
let mut n = Notifications::new();
219
- n.push(Notification::new("Pliki", "Kopiowanie", "12 plików"));
213
+ n.push(Notification::new("Pliki", "Kopiowanie", "12 plików").with_icon(IconName::Files));
220
214
n.advance(0.3);
221
215
n.paint(&mut ctx, Rect::from_xywh(0.0, 40.0, 1280.0, 200.0));
222
216
assert!(scene.len() > 5);
@@ -4,7 +4,7 @@
4
4
5
5
use libluna_core::style::weight;
6
6
use libluna_core::Rect;
7
-use libluna_widgets::PaintCtx;
7
+use libluna_widgets::{paint_icon, IconName, PaintCtx};
8
8
9
9
/// Czas wchodzenia (s).
10
10
const FADE_IN: f64 = 0.18;
@@ -14,8 +14,8 @@ const FADE_OUT: f64 = 0.45;
14
14
/// Wskaźnik OSD.
15
15
#[derive(Debug, Clone)]
16
16
pub struct Osd {
17
- /// Symbol rysowany fontem interfejsu.
18
- pub glyph: String,
17
+ /// Ikona z pakietu Luny.
18
+ pub icon: IconName,
19
19
/// Etykieta (np. „Głośność”).
20
20
pub label: String,
21
21
/// Wartość 0..1 (poziom).
@@ -26,9 +26,9 @@ pub struct Osd {
26
26
27
27
impl Osd {
28
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 {
29
+ pub fn new(icon: IconName, label: impl Into<String>, value: f64) -> Self {
30
30
Self {
31
- glyph: glyph.into(),
31
+ icon,
32
32
label: label.into(),
33
33
value: value.clamp(0.0, 1.0),
34
34
age: 0.0,
@@ -80,14 +80,8 @@ impl Osd {
80
80
let icon = Rect::from_xywh(rect.x() + pad, cy - 20.0, 40.0, 40.0);
81
81
ctx.scene
82
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
- );
83
+ let icon_inner = Rect::from_xywh(icon.x() + 10.0, icon.y() + 10.0, 20.0, 20.0);
84
+ paint_icon(ctx, self.icon, icon_inner, t.colors.accent.scale_alpha(a));
91
85
92
86
// Etykieta i wartość procentowa.
93
87
let text_x = icon.right() + t.spacing.md;
@@ -133,7 +127,7 @@ mod tests {
133
127
134
128
#[test]
135
129
fn fades_in_and_out() {
136
- let mut osd = Osd::new("◆", "Głośność", 0.5);
130
+ let mut osd = Osd::new(IconName::Volume, "Głośność", 0.5);
137
131
assert!(osd.visible());
138
132
assert!(osd.opacity() < 0.05); // dopiero start
139
133
osd.advance(0.3);
@@ -149,7 +143,7 @@ mod tests {
149
143
let text = TextEngine::none();
150
144
let mut scene = Scene::new();
151
145
let mut ctx = PaintCtx::new(&mut scene, &theme, &text);
152
- let mut osd = Osd::new("◆", "Głośność", 0.65);
146
+ let mut osd = Osd::new(IconName::Volume, "Głośność", 0.65);
153
147
osd.advance(0.3);
154
148
osd.paint(&mut ctx, Rect::from_xywh(0.0, 0.0, 320.0, 72.0));
155
149
assert!(scene.len() > 5);
@@ -127,6 +127,8 @@ impl LauncherView {
127
127
pub struct Launcher {
128
128
pub view: LauncherView,
129
129
pub carousel: usize,
130
+ /// Zaznaczona pozycja (nawigacja klawiaturą); `None` = brak zaznaczenia.
131
+ pub selected: Option<usize>,
130
132
/// Pionowe przewinięcie zawartości (Lista/Siatka), w pikselach.
131
133
pub scroll: f64,
132
134
}
@@ -140,6 +142,41 @@ impl Launcher {
140
142
pub fn next_view(&mut self) {
141
143
self.view = self.view.next();
142
144
}
145
+
146
+ /// Bieżący indeks zaznaczenia (domyślnie 0, o ile są pozycje).
147
+ pub fn selected_index(&self, count: usize) -> Option<usize> {
148
+ if count == 0 {
149
+ None
150
+ } else {
151
+ Some(self.selected.unwrap_or(0).min(count - 1))
152
+ }
153
+ }
154
+
155
+ /// Przesuwa zaznaczenie o krok (`dx`, `dy`) zależnie od widoku.
156
+ ///
157
+ /// W siatce `dx`/`dy` to ruch odpowiednio w kolumnie/wierszu; w liście
158
+ /// i karuzeli używany jest tylko jeden z nich (`dy`/`dx`).
159
+ pub fn navigate(&mut self, count: usize, dx: i64, dy: i64) {
160
+ if count == 0 {
161
+ return;
162
+ }
163
+ let n = count as i64;
164
+ let cur = self.selected.unwrap_or(0).min(count - 1) as i64;
165
+ let next = match self.view {
166
+ LauncherView::Grid => {
167
+ let cols = GRID_COLS as i64;
168
+ let rows = (n + cols - 1) / cols;
169
+ let col = (cur % cols + dx).clamp(0, cols - 1);
170
+ let row = (cur / cols + dy).clamp(0, rows - 1);
171
+ (row * cols + col).min(n - 1)
172
+ }
173
+ _ => (cur + dx + dy).rem_euclid(n),
174
+ };
175
+ self.selected = Some(next as usize);
176
+ if self.view == LauncherView::Carousel {
177
+ self.carousel = next as usize;
178
+ }
179
+ }
143
180
}
144
181
145
182
/// Pozycja w launcherze.
@@ -502,6 +539,42 @@ pub fn launcher_scroll_max(
502
539
(total - content.height()).max(0.0)
503
540
}
504
541
542
+/// Dopasowuje przewinięcie tak, aby zaznaczona pozycja była widoczna.
543
+pub fn ensure_selection_visible(
544
+ apps: &[App],
545
+ launcher: &mut Launcher,
546
+ area: Rect,
547
+ theme: &Theme,
548
+ text: &TextEngine,
549
+) {
550
+ let Some(i) = launcher.selected else {
551
+ return;
552
+ };
553
+ if i >= apps.len() {
554
+ return;
555
+ }
556
+ let content = launcher_rects(area, theme, text).content;
557
+ let (top, bottom) = match launcher.view {
558
+ LauncherView::Grid => {
559
+ let t = grid_tile(content, i);
560
+ (t.y() - content.y(), t.bottom() - content.y())
561
+ }
562
+ LauncherView::List => {
563
+ let t = list_row(content, i);
564
+ (t.y() - content.y(), t.bottom() - content.y())
565
+ }
566
+ LauncherView::Carousel => return,
567
+ };
568
+ let view_h = content.height();
569
+ if top < launcher.scroll {
570
+ launcher.scroll = top;
571
+ } else if bottom > launcher.scroll + view_h {
572
+ launcher.scroll = bottom - view_h;
573
+ }
574
+ let max = launcher_scroll_max(apps, launcher, area, theme, text);
575
+ launcher.scroll = launcher.scroll.clamp(0.0, max);
576
+}
577
+
505
578
/// Rysuje launcher w zadanym prostokącie.
506
579
pub fn paint_launcher(apps: &[App], launcher: &Launcher, ctx: &mut PaintCtx, area: Rect) {
507
580
let t = *ctx.theme;
@@ -549,6 +622,7 @@ pub fn paint_launcher(apps: &[App], launcher: &Launcher, ctx: &mut PaintCtx, are
549
622
let t = grid_tile(r.content, i);
550
623
paint_grid_tile(
551
624
app,
625
+ launcher.selected_index(apps.len()) == Some(i),
552
626
ctx,
553
627
Rect::from_xywh(t.x(), t.y() - scroll, t.width(), t.height()),
554
628
);
@@ -559,6 +633,7 @@ pub fn paint_launcher(apps: &[App], launcher: &Launcher, ctx: &mut PaintCtx, are
559
633
let t = list_row(r.content, i);
560
634
paint_list_row(
561
635
app,
636
+ launcher.selected_index(apps.len()) == Some(i),
562
637
ctx,
563
638
Rect::from_xywh(t.x(), t.y() - scroll, t.width(), t.height()),
564
639
);
@@ -578,13 +653,26 @@ pub fn paint_launcher(apps: &[App], launcher: &Launcher, ctx: &mut PaintCtx, are
578
653
ctx.scene.pop_clip();
579
654
}
580
655
581
-/// Kafelek siatki: ikona + nazwa.
582
-fn paint_grid_tile(app: &App, ctx: &mut PaintCtx, rect: Rect) {
656
+/// Kafelek siatki: ikona + nazwa; zaznaczony wyróżniony akcentem.
657
+fn paint_grid_tile(app: &App, selected: bool, ctx: &mut PaintCtx, rect: Rect) {
583
658
let t = ctx.theme;
584
- ctx.scene
585
- .fill_rounded_rect(rect, t.radius.medium, t.colors.view_bg);
586
- ctx.scene
587
- .stroke_rounded_rect(rect, t.radius.medium, 1.0, t.colors.border);
659
+ let bg = if selected {
660
+ t.colors.accent.scale_alpha(0.12)
661
+ } else {
662
+ t.colors.view_bg
663
+ };
664
+ ctx.scene.fill_rounded_rect(rect, t.radius.medium, bg);
665
+ let border = if selected {
666
+ t.colors.accent
667
+ } else {
668
+ t.colors.border
669
+ };
670
+ ctx.scene.stroke_rounded_rect(
671
+ rect,
672
+ t.radius.medium,
673
+ if selected { 2.0 } else { 1.0 },
674
+ border,
675
+ );
588
676
589
677
let icon = Rect::from_xywh(rect.center().x - 18.0, rect.y() + 12.0, 36.0, 36.0);
590
678
ctx.scene
@@ -601,13 +689,26 @@ fn paint_grid_tile(app: &App, ctx: &mut PaintCtx, rect: Rect) {
601
689
);
602
690
}
603
691
604
-/// Wiersz listy: ikona + nazwa + kategoria.
605
-fn paint_list_row(app: &App, ctx: &mut PaintCtx, rect: Rect) {
692
+/// Wiersz listy: ikona + nazwa + kategoria; zaznaczony wyróżniony akcentem.
693
+fn paint_list_row(app: &App, selected: bool, ctx: &mut PaintCtx, rect: Rect) {
606
694
let t = ctx.theme;
607
- ctx.scene
608
- .fill_rounded_rect(rect, t.radius.medium, t.colors.view_bg);
609
- ctx.scene
610
- .stroke_rounded_rect(rect, t.radius.medium, 1.0, t.colors.border);
695
+ let bg = if selected {
696
+ t.colors.accent.scale_alpha(0.12)
697
+ } else {
698
+ t.colors.view_bg
699
+ };
700
+ ctx.scene.fill_rounded_rect(rect, t.radius.medium, bg);
701
+ let border = if selected {
702
+ t.colors.accent
703
+ } else {
704
+ t.colors.border
705
+ };
706
+ ctx.scene.stroke_rounded_rect(
707
+ rect,
708
+ t.radius.medium,
709
+ if selected { 2.0 } else { 1.0 },
710
+ border,
711
+ );
611
712
612
713
let icon = Rect::from_xywh(rect.x() + t.spacing.md, rect.center().y - 16.0, 32.0, 32.0);
613
714
ctx.scene
@@ -739,6 +840,7 @@ mod tests {
739
840
let launcher = Launcher {
740
841
view,
741
842
carousel: 2,
843
+ selected: None,
742
844
scroll: 0.0,
743
845
};
744
846
paint_launcher(&App::sample(), &launcher, &mut ctx, area());
@@ -778,6 +880,7 @@ mod tests {
778
880
let mut launcher = Launcher {
779
881
view: LauncherView::List,
780
882
carousel: 0,
883
+ selected: None,
781
884
scroll: 0.0,
782
885
};
783
886
let max = launcher_scroll_max(&apps, &launcher, area(), &theme, &text);
@@ -809,6 +912,7 @@ mod tests {
809
912
let launcher = Launcher {
810
913
view,
811
914
carousel: 2,
915
+ selected: None,
812
916
scroll: 0.0,
813
917
};
814
918
let r = launcher_rects(area(), &theme, &text);
@@ -826,4 +930,58 @@ mod tests {
826
930
);
827
931
}
828
932
}
933
+
934
+ #[test]
935
+ fn keyboard_navigation_moves_selection_per_view() {
936
+ // Lista: +1/-1 po indeksie, z zawijaniem.
937
+ let mut list = Launcher {
938
+ view: LauncherView::List,
939
+ ..Launcher::default()
940
+ };
941
+ assert_eq!(list.selected_index(0), None);
942
+ list.navigate(5, 0, 1);
943
+ assert_eq!(list.selected_index(5), Some(1));
944
+ list.navigate(5, 0, -1);
945
+ list.navigate(5, 0, -1);
946
+ assert_eq!(list.selected_index(5), Some(4)); // zawija
947
+
948
+ // Siatka: pion = ±GRID_COLS, poziom = ±1.
949
+ let mut grid = Launcher {
950
+ view: LauncherView::Grid,
951
+ ..Launcher::default()
952
+ };
953
+ grid.navigate(24, 0, 1);
954
+ assert_eq!(grid.selected_index(24), Some(GRID_COLS));
955
+ grid.navigate(24, 1, 0);
956
+ assert_eq!(grid.selected_index(24), Some(GRID_COLS + 1));
957
+ grid.navigate(24, 0, -5);
958
+ assert_eq!(grid.selected_index(24), Some(1)); // nie wychodzi nad wiersz 0
959
+
960
+ // Karuzela: steruje indeksem karty.
961
+ let mut car = Launcher {
962
+ view: LauncherView::Carousel,
963
+ ..Launcher::default()
964
+ };
965
+ car.navigate(6, 1, 0);
966
+ assert_eq!(car.selected_index(6), Some(1));
967
+ assert_eq!(car.carousel, 1);
968
+ }
969
+
970
+ #[test]
971
+ fn selection_scrolls_into_view() {
972
+ let theme = Theme::light();
973
+ let text = TextEngine::none();
974
+ let apps = App::sample();
975
+ let mut launcher = Launcher {
976
+ view: LauncherView::List,
977
+ ..Launcher::default()
978
+ };
979
+ // Wybierz ostatnią pozycję i upewnij się, że lista się przewinęła.
980
+ launcher.navigate(apps.len(), 0, -1);
981
+ ensure_selection_visible(&apps, &mut launcher, area(), &theme, &text);
982
+ assert!(
983
+ launcher.scroll > 0.0,
984
+ "zaznaczenie poza ekranem ma przewijać"
985
+ );
986
+ }
829
987
}
830
988
Binary files a/docs/images/luna-launcher-grid.png and b/docs/images/luna-launcher-grid.png differ
831
989
Binary files a/docs/images/luna-shell-dark.png and b/docs/images/luna-shell-dark.png differ
832
990
Binary files a/docs/images/luna-shell-light.png and b/docs/images/luna-shell-light.png differ