← luna

Commit fc34c1c

2
files
+131
added
-24
deleted
@@ -5,13 +5,13 @@ use std::path::{Path, PathBuf};
5 5 use std::sync::Arc;
6 6 use std::time::{Duration, Instant};
7 7
8 -use libluna_core::{ThemePreference, ThemeSettings};
8 +use libluna_core::{Point, ThemePreference, ThemeSettings};
9 9 use libluna_i18n::{set_locale, tr, tr_args, Locale};
10 10 use libluna_platform::{run as run_app, AppConfig, Frame};
11 11 use libluna_render::ImageData;
12 12 use libluna_widgets::{
13 13 AlertDialog, Entry as TextEntry, ItemView, Menu, NavEntry, NavList, PaintCtx, ScrollView,
14 - SearchEntry, Toast, ViewMode,
14 + SearchEntry, Toast, ViewMode, WindowControl,
15 15 };
16 16
17 17 use crate::model::{human_size, now_secs, DirView, Entry, Kind, SortKey};
@@ -276,6 +276,10 @@ pub struct App {
276 276 pub status: String,
277 277 /// Zakładka pod kursorem (podświetlenie w pasku tytułu).
278 278 pub tab_hover: Option<usize>,
279 + /// Ostatnia pozycja kursora w oknie (do podświetlania przycisków).
280 + pub pointer: Option<Point>,
281 + /// Kontrolka okna pod kursorem (— □ ✕).
282 + pub controls_hover: Option<WindowControl>,
279 283 /// Preferencje (język, domyślny widok, ukryte pliki).
280 284 pub prefs: Prefs,
281 285 /// Okno Ustawień (modal).
@@ -332,6 +336,8 @@ impl App {
332 336 last_mount_refresh: Instant::now() - Duration::from_secs(10),
333 337 status: String::new(),
334 338 tab_hover: None,
339 + pointer: None,
340 + controls_hover: None,
335 341 prefs,
336 342 settings_open: false,
337 343 about: None,
@@ -318,7 +318,7 @@ fn paint_header(ctx: &mut PaintCtx, app: &App, lay: &Layout) {
318 318 // Kontrolki okna rysuje widget — bez tytułu, bo w jednym wierszu jest pasek ścieżki.
319 319 HeaderBar::new()
320 320 .controls(ControlsSide::Right)
321 - .paint(ctx, lay.top, None);
321 + .paint(ctx, lay.top, app.controls_hover);
322 322 if multi {
323 323 // Oba wiersze mają wyglądać jak jeden pasek: zamalowuj linię pod górnym.
324 324 ctx.scene.fill_rect(
@@ -341,18 +341,27 @@ fn paint_header(ctx: &mut PaintCtx, app: &App, lay: &Layout) {
341 341
342 342 fn paint_tabs(ctx: &mut PaintCtx, app: &App, lay: &Layout) {
343 343 tab_bar(app).paint(ctx, lay.tabs);
344 - icon_button(ctx, lay.plus, IconName::Plus, true, false);
344 + icon_button(
345 + ctx,
346 + lay.plus,
347 + IconName::Plus,
348 + true,
349 + false,
350 + is_hovered(app, lay.plus),
351 + );
345 352 }
346 353
347 354 fn paint_nav(ctx: &mut PaintCtx, app: &App, ch: &Chrome) {
348 355 let pane = app.pane();
349 356
350 357 // Przełącznik paska bocznego (jak w aplikacjach libLuny).
351 - ctx.scene.fill_rounded_rect(
352 - ch.sidebar_toggle,
353 - ctx.theme.radius.small,
354 - ctx.theme.colors.shade,
355 - );
358 + let sidebar_bg = if is_hovered(app, ch.sidebar_toggle) {
359 + ctx.theme.colors.shade.scale_alpha(1.6)
360 + } else {
361 + ctx.theme.colors.shade
362 + };
363 + ctx.scene
364 + .fill_rounded_rect(ch.sidebar_toggle, ctx.theme.radius.small, sidebar_bg);
356 365 sidebar_icon(
357 366 ctx,
358 367 ctx.theme.colors.text,
@@ -366,6 +375,7 @@ fn paint_nav(ctx: &mut PaintCtx, app: &App, ch: &Chrome) {
366 375 IconName::ChevronLeft,
367 376 !pane.back.is_empty(),
368 377 false,
378 + is_hovered(app, ch.back),
369 379 );
370 380 icon_button(
371 381 ctx,
@@ -373,8 +383,14 @@ fn paint_nav(ctx: &mut PaintCtx, app: &App, ch: &Chrome) {
373 383 IconName::ChevronRight,
374 384 !pane.forward.is_empty(),
375 385 false,
386 + is_hovered(app, ch.forward),
387 + );
388 + icon_up(
389 + ctx,
390 + ch.up,
391 + pane.dir.parent().is_some(),
392 + is_hovered(app, ch.up),
376 393 );
377 - icon_up(ctx, ch.up, pane.dir.parent().is_some());
378 394
379 395 if let Some(edit) = &pane.edit {
380 396 edit.entry.paint(ctx, ch.path);
@@ -385,13 +401,34 @@ fn paint_nav(ctx: &mut PaintCtx, app: &App, ch: &Chrome) {
385 401 bar.paint(ctx, ch.path);
386 402 }
387 403
388 - icon_button(ctx, ch.search, IconName::Search, true, pane.search_active);
404 + icon_button(
405 + ctx,
406 + ch.search,
407 + IconName::Search,
408 + true,
409 + pane.search_active,
410 + is_hovered(app, ch.search),
411 + );
389 412 let view_icon = match pane.view.mode() {
390 413 ViewMode::List => IconName::Grid,
391 414 ViewMode::Grid => IconName::List,
392 415 };
393 - icon_button(ctx, ch.view, view_icon, true, false);
394 - icon_button(ctx, ch.menu, IconName::Menu, true, false);
416 + icon_button(
417 + ctx,
418 + ch.view,
419 + view_icon,
420 + true,
421 + false,
422 + is_hovered(app, ch.view),
423 + );
424 + icon_button(
425 + ctx,
426 + ch.menu,
427 + IconName::Menu,
428 + true,
429 + false,
430 + is_hovered(app, ch.menu),
431 + );
395 432 }
396 433
397 434 fn paint_sidebar(ctx: &mut PaintCtx, app: &App, lay: &Layout) {
@@ -653,16 +690,21 @@ fn paint_status(ctx: &mut PaintCtx, app: &App, lay: &Layout) {
653 690 }
654 691 }
655 692
656 -fn icon_button(ctx: &mut PaintCtx, rect: Rect, name: IconName, enabled: bool, active: bool) {
657 - if active || enabled {
658 - let bg = if active {
659 - ctx.theme.colors.shade
660 - } else {
661 - Color::TRANSPARENT
662 - };
663 - if active {
664 - ctx.scene.fill_rounded_rect(rect, 6.0, bg);
665 - }
693 +fn icon_button(
694 + ctx: &mut PaintCtx,
695 + rect: Rect,
696 + name: IconName,
697 + enabled: bool,
698 + active: bool,
699 + hover: bool,
700 +) {
701 + if enabled && !active && hover {
702 + ctx.scene
703 + .fill_rounded_rect(rect, 6.0, ctx.theme.colors.shade);
704 + }
705 + if active && enabled {
706 + ctx.scene
707 + .fill_rounded_rect(rect, 6.0, ctx.theme.colors.shade);
666 708 }
667 709 let color = if enabled {
668 710 ctx.theme.colors.text
@@ -672,6 +714,11 @@ fn icon_button(ctx: &mut PaintCtx, rect: Rect, name: IconName, enabled: bool, ac
672 714 icon::paint_icon(ctx, name, rect.deflate(Insets::splat(6.0)), color);
673 715 }
674 716
717 +/// Czy kursor jest nad prostokątem (dla podświetlenia przycisków).
718 +fn is_hovered(app: &App, rect: Rect) -> bool {
719 + app.pointer.is_some_and(|p| rect.contains(p))
720 +}
721 +
675 722 /// Ikona przełącznika paska bocznego (◧ / ▭) — jak w pozostałych aplikacjach Luny.
676 723 fn sidebar_icon(ctx: &mut PaintCtx, color: Color, box_: Rect, filled: bool) {
677 724 let c = box_.center();
@@ -684,7 +731,11 @@ fn sidebar_icon(ctx: &mut PaintCtx, color: Color, box_: Rect, filled: bool) {
684 731 }
685 732 }
686 733
687 -fn icon_up(ctx: &mut PaintCtx, rect: Rect, enabled: bool) {
734 +fn icon_up(ctx: &mut PaintCtx, rect: Rect, enabled: bool, hover: bool) {
735 + if enabled && hover {
736 + ctx.scene
737 + .fill_rounded_rect(rect, 6.0, ctx.theme.colors.shade);
738 + }
688 739 let color = if enabled {
689 740 ctx.theme.colors.text
690 741 } else {
@@ -1043,6 +1094,16 @@ pub fn handle_events(
1043 1094 },
1044 1095 Event::PointerMotion { position, .. } => {
1045 1096 let p = *position;
1097 + app.pointer = Some(p);
1098 + // Podświetlenie kontrolek okna (— □ ✕): `HeaderBar::paint` rysuje je
1099 + // tylko dla kontrolki przekazanej jako `hovered`.
1100 + app.controls_hover = if lay.top.contains(p) {
1101 + HeaderBar::new()
1102 + .controls(ControlsSide::Right)
1103 + .control_at(lay.top, theme, p)
1104 + } else {
1105 + None
1106 + };
1046 1107 if let Some(context) = app.context.as_mut() {
1047 1108 context.menu.hover_at(context.panel, Some(p), theme);
1048 1109 continue;
@@ -1068,6 +1129,8 @@ pub fn handle_events(
1068 1129 }
1069 1130 }
1070 1131 Event::PointerLeave => {
1132 + app.pointer = None;
1133 + app.controls_hover = None;
1071 1134 app.tab_hover = None;
1072 1135 app.sidebar.set_hovered(None);
1073 1136 for pane in &mut app.panes {
@@ -2214,6 +2277,44 @@ mod tests {
2214 2277 );
2215 2278 }
2216 2279
2280 + #[test]
2281 + fn window_controls_track_hover() {
2282 + let mut app = app();
2283 + let theme = Theme::light();
2284 + let text = TextEngine::system();
2285 + let size = Size::new(1120.0, 720.0);
2286 + let lay = layout(&app, size, &theme, &text);
2287 + let close = HeaderBar::new()
2288 + .controls(ControlsSide::Right)
2289 + .controls_layout(lay.top, &theme)
2290 + .close
2291 + .unwrap();
2292 + handle_events(
2293 + &mut app,
2294 + &[Event::PointerMotion {
2295 + position: close.center(),
2296 + modifiers: Modifiers::default(),
2297 + }],
2298 + size,
2299 + &theme,
2300 + &text,
2301 + );
2302 + assert_eq!(app.controls_hover, Some(WindowControl::Close));
2303 + assert!(app.pointer.is_some());
2304 +
2305 + handle_events(
2306 + &mut app,
2307 + &[Event::PointerMotion {
2308 + position: Point::new(close.center().x, close.center().y + 300.0),
2309 + modifiers: Modifiers::default(),
2310 + }],
2311 + size,
2312 + &theme,
2313 + &text,
2314 + );
2315 + assert_eq!(app.controls_hover, None);
2316 + }
2317 +
2217 2318 #[test]
2218 2319 fn middle_click_opens_folder_in_new_tab() {
2219 2320 let dir = std::env::temp_dir().join("luna-files-middle-click");