Commit ac72694
8
files
+134
added
-35
deleted
@@ -29,6 +29,7 @@ fn main() -> Result<(), Box<dyn Error>> {
29
29
let launcher = Launcher {
30
30
view: LauncherView::Grid,
31
31
carousel: 0,
32
+ scroll: 0.0,
32
33
};
33
34
panel::paint_desktop(&pstate, &apps, &launcher, &mut paint, size);
34
35
@@ -55,7 +56,7 @@ fn main() -> Result<(), Box<dyn Error>> {
55
56
)?;
56
57
}
57
58
58
- // 2) Launcher w trzech widokach (ciemny motyw).
59
+ // 2) Launcher w trzech widokach (ciemny motyw). Lista pokazuje przewinięcie.
59
60
for (name, view) in [
60
61
("list", LauncherView::List),
61
62
("grid", LauncherView::Grid),
@@ -64,7 +65,15 @@ fn main() -> Result<(), Box<dyn Error>> {
64
65
let theme = Theme::dark();
65
66
let mut scene = Scene::new();
66
67
let mut paint = PaintCtx::new(&mut scene, &theme, &text);
67
- let launcher = Launcher { view, carousel: 2 };
68
+ let launcher = Launcher {
69
+ view,
70
+ carousel: 2,
71
+ scroll: if view == LauncherView::List {
72
+ 140.0
73
+ } else {
74
+ 0.0
75
+ },
76
+ };
68
77
panel::paint_desktop(&pstate, &apps, &launcher, &mut paint, size);
69
78
save(
70
79
&scene,
@@ -52,17 +52,27 @@ fn main() -> Result<(), Box<dyn Error>> {
52
52
// ── Obsługa wejścia ──
53
53
for ev in frame.events {
54
54
match ev {
55
- Event::PointerButton(pe) if pe.state == ButtonState::Pressed => match pe.button {
56
- PointerButton::Left
57
- if panel::burger_rect(bar, &frame.theme).contains(pe.position) =>
58
- {
55
+ Event::PointerButton(pe)
56
+ if pe.state == ButtonState::Pressed && pe.button == PointerButton::Left =>
57
+ {
58
+ if panel::burger_rect(bar, &frame.theme).contains(pe.position) {
59
59
open = !open;
60
- }
61
- PointerButton::Left if open => {
60
+ if open {
61
+ let max = panel::launcher_scroll_max(
62
+ &apps,
63
+ &launcher,
64
+ larea,
65
+ &frame.theme,
66
+ text,
67
+ );
68
+ launcher.scroll = launcher.scroll.clamp(0.0, max);
69
+ }
70
+ } else if open {
62
71
if let Some(v) =
63
72
panel::launcher_view_at(larea, &frame.theme, text, pe.position)
64
73
{
65
74
launcher.view = v;
75
+ launcher.scroll = 0.0;
66
76
} else if panel::launcher_app_at(
67
77
&apps,
68
78
&launcher,
@@ -76,8 +86,17 @@ fn main() -> Result<(), Box<dyn Error>> {
76
86
open = false;
77
87
}
78
88
}
79
- _ => {}
80
- },
89
+ }
90
+ Event::Scroll(se) if open => {
91
+ if panel::launcher_rects(larea, &frame.theme, text)
92
+ .content
93
+ .contains(se.position)
94
+ {
95
+ let max =
96
+ panel::launcher_scroll_max(&apps, &launcher, larea, &frame.theme, text);
97
+ launcher.scroll = (launcher.scroll - se.dy * 24.0).clamp(0.0, max);
98
+ }
99
+ }
81
100
Event::Key(ke) if ke.keysym == 0xff1b => open = false, // Escape
82
101
_ => {}
83
102
}
@@ -122,11 +122,13 @@ impl LauncherView {
122
122
}
123
123
}
124
124
125
-/// Stan launchera (widok + bieżąca karta karuzeli).
125
+/// Stan launchera (widok + bieżąca karta karuzeli + przewinięcie).
126
126
#[derive(Debug, Clone, Default)]
127
127
pub struct Launcher {
128
128
pub view: LauncherView,
129
129
pub carousel: usize,
130
+ /// Pionowe przewinięcie zawartości (Lista/Siatka), w pikselach.
131
+ pub scroll: f64,
130
132
}
131
133
132
134
impl Launcher {
@@ -153,7 +155,7 @@ pub struct App {
153
155
impl App {
154
156
/// Przykładowy zestaw ikon (podglądy).
155
157
pub fn sample() -> Vec<App> {
156
- const APPS: [(IconName, &str, &str); 8] = [
158
+ const APPS: [(IconName, &str, &str); 12] = [
157
159
(IconName::Files, "Pliki", "System"),
158
160
(IconName::Settings, "Ustawienia", "System"),
159
161
(IconName::Edit, "Edytor", "Akcesoria"),
@@ -162,6 +164,10 @@ impl App {
162
164
(IconName::Clock, "Zegar", "Akcesoria"),
163
165
(IconName::Terminal, "Terminal", "System"),
164
166
(IconName::Store, "Sklep", "System"),
167
+ (IconName::Wifi, "Sieć", "System"),
168
+ (IconName::Home, "Katalog", "System"),
169
+ (IconName::User, "Konta", "System"),
170
+ (IconName::Info, "Pomoc", "System"),
165
171
];
166
172
APPS.iter()
167
173
.map(|(i, n, c)| App {
@@ -409,9 +415,16 @@ pub fn launcher_app_at(
409
415
p: Point,
410
416
) -> Option<usize> {
411
417
let content = launcher_rects(area, theme, text).content;
418
+ let sy = launcher.scroll;
412
419
match launcher.view {
413
- LauncherView::Grid => (0..apps.len()).find(|&i| grid_tile(content, i).contains(p)),
414
- LauncherView::List => (0..apps.len()).find(|&i| list_row(content, i).contains(p)),
420
+ LauncherView::Grid => (0..apps.len()).find(|&i| {
421
+ let t = grid_tile(content, i);
422
+ Rect::from_xywh(t.x(), t.y() - sy, t.width(), t.height()).contains(p)
423
+ }),
424
+ LauncherView::List => (0..apps.len()).find(|&i| {
425
+ let t = list_row(content, i);
426
+ Rect::from_xywh(t.x(), t.y() - sy, t.width(), t.height()).contains(p)
427
+ }),
415
428
LauncherView::Carousel => {
416
429
(0..apps.len()).find(|&i| carousel_card(content, i, launcher.carousel).contains(p))
417
430
}
@@ -452,6 +465,28 @@ fn carousel_card(content: Rect, i: usize, selected: usize) -> Rect {
452
465
)
453
466
}
454
467
468
+/// Maksymalne pionowe przewinięcie dla danego widoku.
469
+pub fn launcher_scroll_max(
470
+ apps: &[App],
471
+ launcher: &Launcher,
472
+ area: Rect,
473
+ theme: &Theme,
474
+ text: &TextEngine,
475
+) -> f64 {
476
+ let content = launcher_rects(area, theme, text).content;
477
+ let gap_list = 8.0;
478
+ let gap_grid = 12.0;
479
+ let total = match launcher.view {
480
+ LauncherView::List => (apps.len() as f64) * (LIST_ROW_H + gap_list) - gap_list,
481
+ LauncherView::Grid => {
482
+ let rows = apps.len().div_ceil(GRID_COLS) as f64;
483
+ rows * (GRID_CELL_H + gap_grid) - gap_grid
484
+ }
485
+ LauncherView::Carousel => 0.0,
486
+ };
487
+ (total - content.height()).max(0.0)
488
+}
489
+
455
490
/// Rysuje launcher w zadanym prostokącie.
456
491
pub fn paint_launcher(apps: &[App], launcher: &Launcher, ctx: &mut PaintCtx, area: Rect) {
457
492
let t = *ctx.theme;
@@ -490,37 +525,42 @@ pub fn paint_launcher(apps: &[App], launcher: &Launcher, ctx: &mut PaintCtx, are
490
525
t.colors.dim_text,
491
526
);
492
527
493
- // Liczba pozycji mieszczących się w obszarze zawartości (bez przewijania).
494
- let max_grid = (((r.content.height() + 12.0) / (GRID_CELL_H + 12.0))
495
- .floor()
496
- .max(0.0) as usize)
497
- * GRID_COLS;
498
- let max_list = ((r.content.height() + 8.0) / (LIST_ROW_H + 8.0))
499
- .floor()
500
- .max(0.0) as usize;
501
-
528
+ // Zawartość przewijana pionowo (Lista/Siatka) — obcinana do karty.
529
+ let scroll = launcher.scroll;
530
+ ctx.scene.push_clip(r.content);
502
531
match launcher.view {
503
532
LauncherView::Grid => {
504
- for (i, app) in apps.iter().take(max_grid).enumerate() {
505
- paint_grid_tile(app, ctx, grid_tile(r.content, i));
533
+ for (i, app) in apps.iter().enumerate() {
534
+ let t = grid_tile(r.content, i);
535
+ paint_grid_tile(
536
+ app,
537
+ ctx,
538
+ Rect::from_xywh(t.x(), t.y() - scroll, t.width(), t.height()),
539
+ );
506
540
}
507
541
}
508
542
LauncherView::List => {
509
- for (i, app) in apps.iter().take(max_list).enumerate() {
510
- paint_list_row(app, ctx, list_row(r.content, i));
543
+ for (i, app) in apps.iter().enumerate() {
544
+ let t = list_row(r.content, i);
545
+ paint_list_row(
546
+ app,
547
+ ctx,
548
+ Rect::from_xywh(t.x(), t.y() - scroll, t.width(), t.height()),
549
+ );
511
550
}
512
551
}
513
552
LauncherView::Carousel => {
514
553
for (i, app) in apps.iter().enumerate() {
515
- let card = carousel_card(r.content, i, launcher.carousel);
516
- // Rysujemy tylko karty mieszczące się całe w obszarze (brak obcinania tekstu).
517
- if card.x() < r.content.x() || card.right() > r.content.right() {
518
- continue;
519
- }
520
- paint_carousel_card(app, i == launcher.carousel, ctx, card);
554
+ paint_carousel_card(
555
+ app,
556
+ i == launcher.carousel,
557
+ ctx,
558
+ carousel_card(r.content, i, launcher.carousel),
559
+ );
521
560
}
522
561
}
523
562
}
563
+ ctx.scene.pop_clip();
524
564
}
525
565
526
566
/// Kafelek siatki: ikona + nazwa.
@@ -681,7 +721,11 @@ mod tests {
681
721
&mut ctx,
682
722
Rect::from_xywh(0.0, 0.0, 1280.0, THICKNESS),
683
723
);
684
- let launcher = Launcher { view, carousel: 2 };
724
+ let launcher = Launcher {
725
+ view,
726
+ carousel: 2,
727
+ scroll: 0.0,
728
+ };
685
729
paint_launcher(&App::sample(), &launcher, &mut ctx, area());
686
730
scene.len()
687
731
}
@@ -711,6 +755,29 @@ mod tests {
711
755
assert_eq!(LauncherView::Carousel.next(), LauncherView::List);
712
756
}
713
757
758
+ #[test]
759
+ fn list_scroll_is_clamped_and_offsets_hit_test() {
760
+ let theme = Theme::light();
761
+ let text = TextEngine::none();
762
+ let apps = App::sample();
763
+ let mut launcher = Launcher {
764
+ view: LauncherView::List,
765
+ carousel: 0,
766
+ scroll: 0.0,
767
+ };
768
+ let max = launcher_scroll_max(&apps, &launcher, area(), &theme, &text);
769
+ assert!(max > 0.0, "lista 12 pozycji powinna się przewijać");
770
+
771
+ // Po przewinięciu o jeden wiersz pierwsza pozycja jest nad obszarem → trafiamy w drugą.
772
+ launcher.scroll = (LIST_ROW_H + 8.0).min(max);
773
+ let r = launcher_rects(area(), &theme, &text);
774
+ let top = Point::new(r.content.x() + 5.0, r.content.y() + 5.0);
775
+ assert_eq!(
776
+ launcher_app_at(&apps, &launcher, area(), &theme, &text, top),
777
+ Some(1)
778
+ );
779
+ }
780
+
714
781
#[test]
715
782
fn all_three_views_paint() {
716
783
assert!(panel_commands(LauncherView::List) > 20);
@@ -724,7 +791,11 @@ mod tests {
724
791
let text = TextEngine::none();
725
792
let apps = App::sample();
726
793
for view in LauncherView::ALL {
727
- let launcher = Launcher { view, carousel: 2 };
794
+ let launcher = Launcher {
795
+ view,
796
+ carousel: 2,
797
+ scroll: 0.0,
798
+ };
728
799
let r = launcher_rects(area(), &theme, &text);
729
800
// Punkt wnętrza znanej pozycji (pierwszy kafel/wiersz, albo środek wybranej karty).
730
801
let (p, expected) = match view {
731
802
Binary files a/docs/images/luna-launcher-carousel.png and b/docs/images/luna-launcher-carousel.png differ
732
803
Binary files a/docs/images/luna-launcher-grid.png and b/docs/images/luna-launcher-grid.png differ
733
804
Binary files a/docs/images/luna-launcher-list.png and b/docs/images/luna-launcher-list.png differ
734
805
Binary files a/docs/images/luna-shell-dark.png and b/docs/images/luna-shell-dark.png differ
735
806
Binary files a/docs/images/luna-shell-light.png and b/docs/images/luna-shell-light.png differ