← luna

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