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");