diff --git a/packages/oneclient_app/src/components/button.rs b/packages/oneclient_app/src/components/button.rs index 8699a0103..8b2c735a8 100644 --- a/packages/oneclient_app/src/components/button.rs +++ b/packages/oneclient_app/src/components/button.rs @@ -327,7 +327,7 @@ impl Component for Button { }) .cursor(cursor_icon) .map(on_press.clone(), |rect, handler| { - rect.on_all_press(move |event: Event| { + rect.on_press(move |event: Event| { handler.call(event); }) }); diff --git a/packages/oneclient_app/src/components/dropdown.rs b/packages/oneclient_app/src/components/dropdown.rs index 5ebecd2cc..4099d3859 100644 --- a/packages/oneclient_app/src/components/dropdown.rs +++ b/packages/oneclient_app/src/components/dropdown.rs @@ -1,6 +1,7 @@ use freya::prelude::*; use crate::components::{Icon, IconType, ScrollArea}; +use crate::hooks::use_overlay_claim_when; use crate::theme::colors; use crate::ui::border_all_color; @@ -70,6 +71,8 @@ impl Component for Dropdown { let on_select = self.on_select.clone(); let is_open = open(); + use_overlay_claim_when(is_open); + if !is_open && list_size().is_some() { let _ = list_size.take(); } @@ -108,6 +111,11 @@ impl Component for Dropdown { rect() .width(self.width.clone()) + .on_global_key_down(move |e: Event| { + if e.key == Key::Named(NamedKey::Escape) { + open.set_if_modified(false); + } + }) .child( rect() .width(Size::fill()) @@ -130,7 +138,7 @@ impl Component for Dropdown { .on_sized(move |e: Event| { button_area.set_if_modified(Some(e.area)); }) - .on_all_press(move |e: Event| { + .on_press(move |e: Event| { e.prevent_default(); e.stop_propagation(); open.toggle(); @@ -237,7 +245,7 @@ impl Component for DropdownOption { }) .on_pointer_enter(move |_| hovering.set(true)) .on_pointer_leave(move |_| hovering.set(false)) - .on_all_press(self.on_press.clone()) + .on_press(self.on_press.clone()) .child( label() .text(self.text.clone()) diff --git a/packages/oneclient_app/src/components/navbar/app_navbar.rs b/packages/oneclient_app/src/components/navbar/app_navbar.rs index 76a148a74..a49a39fa1 100644 --- a/packages/oneclient_app/src/components/navbar/app_navbar.rs +++ b/packages/oneclient_app/src/components/navbar/app_navbar.rs @@ -189,7 +189,7 @@ impl Component for NavLink { .a11y_id(a11y_id) .a11y_focusable(true) .a11y_role(AccessibilityRole::Button) - .on_all_press(move |e: Event| { + .on_press(move |e: Event| { e.prevent_default(); match &target { NavTarget::Route(route) => { diff --git a/packages/oneclient_app/src/components/overlay_popup.rs b/packages/oneclient_app/src/components/overlay_popup.rs index 3b2f776c6..33e50c4d7 100644 --- a/packages/oneclient_app/src/components/overlay_popup.rs +++ b/packages/oneclient_app/src/components/overlay_popup.rs @@ -3,6 +3,8 @@ use freya::{ prelude::*, }; +use crate::hooks::use_overlay_claim; + const SCRIM_ALPHA: f32 = 90.; #[derive(PartialEq)] @@ -61,6 +63,8 @@ impl KeyExt for OverlayPopup { impl Component for OverlayPopup { fn render(&self) -> impl IntoElement { + use_overlay_claim(); + let a11y_id = use_a11y(); let scrim_close = self.on_close.clone(); let key_close = self.on_close.clone(); diff --git a/packages/oneclient_app/src/components/recents_row.rs b/packages/oneclient_app/src/components/recents_row.rs index 9b735dcf3..a7d613a4c 100644 --- a/packages/oneclient_app/src/components/recents_row.rs +++ b/packages/oneclient_app/src/components/recents_row.rs @@ -173,7 +173,7 @@ impl Component for ClusterCard { .y(0.) .color(Color::from_af32rgb(0.3, 0, 0, 0)), ) - .on_all_press(on_press) + .on_press(on_press) .child( rect() .width(Size::fill()) @@ -281,7 +281,7 @@ impl Component for OtherVersionsTile { .y(0.) .color(Color::from_af32rgb(0.3, 0, 0, 0)), ) - .on_all_press(|_| { + .on_press(|_| { let _ = RouterContext::get().push(Route::Clusters {}); }) .child( diff --git a/packages/oneclient_app/src/components/segmented_control.rs b/packages/oneclient_app/src/components/segmented_control.rs index 917beb310..32b08452d 100644 --- a/packages/oneclient_app/src/components/segmented_control.rs +++ b/packages/oneclient_app/src/components/segmented_control.rs @@ -185,7 +185,7 @@ impl Component for SegmentButton { }) .maybe(!disabled, |el| { el.cursor(CursorIcon::Pointer) - .on_all_press(move |_| *selected.write() = value) + .on_press(move |_| *selected.write() = value) }) .maybe_child(self.icon.map(|icon| { let mut ic = Icon::new(icon); diff --git a/packages/oneclient_app/src/components/tab_bar.rs b/packages/oneclient_app/src/components/tab_bar.rs index 636748fe0..c56cb394b 100644 --- a/packages/oneclient_app/src/components/tab_bar.rs +++ b/packages/oneclient_app/src/components/tab_bar.rs @@ -187,7 +187,7 @@ impl Component for TabButton { .cursor(CursorIcon::Pointer); if let Some(handler) = self.on_press.clone() { - el = el.on_all_press(move |e| handler.call(e)); + el = el.on_press(move |e| handler.call(e)); } el.child( diff --git a/packages/oneclient_app/src/components/toggle.rs b/packages/oneclient_app/src/components/toggle.rs index 30adb1d72..6055819ba 100644 --- a/packages/oneclient_app/src/components/toggle.rs +++ b/packages/oneclient_app/src/components/toggle.rs @@ -106,7 +106,7 @@ impl Component for Switch { .a11y_focusable(!disabled) .a11y_role(AccessibilityRole::Button) .maybe(!disabled, |el| { - el.cursor(CursorIcon::Pointer).on_all_press(move |_| { + el.cursor(CursorIcon::Pointer).on_press(move |_| { on_press.call(()); }) }) diff --git a/packages/oneclient_app/src/components/version_card.rs b/packages/oneclient_app/src/components/version_card.rs index 8695627ac..b8c806e70 100644 --- a/packages/oneclient_app/src/components/version_card.rs +++ b/packages/oneclient_app/src/components/version_card.rs @@ -79,7 +79,7 @@ impl Component for VersionCard { .a11y_id(a11y_id) .a11y_focusable(true) .a11y_role(AccessibilityRole::Button) - .on_all_press(move |e| on_press.call(e)) + .on_press(move |e| on_press.call(e)) .on_pointer_enter(move |_| { *hovering.write() = true; }) diff --git a/packages/oneclient_app/src/hooks/mod.rs b/packages/oneclient_app/src/hooks/mod.rs index a106dd946..2f35dd48a 100644 --- a/packages/oneclient_app/src/hooks/mod.rs +++ b/packages/oneclient_app/src/hooks/mod.rs @@ -1,10 +1,15 @@ mod active_cluster; mod debounce; mod actions; +mod overlay_claims; mod queries; mod view_state; pub use debounce::use_debounced; +pub use overlay_claims::{ + OverlayClaims, use_overlay_claim, use_overlay_claim_when, use_overlay_claims, + use_provide_overlay_claims, +}; pub use view_state::{PersistedView, use_view_state}; pub use active_cluster::{ diff --git a/packages/oneclient_app/src/hooks/overlay_claims.rs b/packages/oneclient_app/src/hooks/overlay_claims.rs new file mode 100644 index 000000000..2955eeb70 --- /dev/null +++ b/packages/oneclient_app/src/hooks/overlay_claims.rs @@ -0,0 +1,60 @@ +use std::cell::Cell; +use std::rc::Rc; + +use freya::prelude::*; + +#[derive(Clone, Default)] +pub struct OverlayClaims(Rc>); + +impl OverlayClaims { + /// Whether anything on screen currently owns Escape and mouse-back. + pub fn any(&self) -> bool { + self.0.get() > 0 + } + + fn acquire(&self) { + self.0.set(self.0.get() + 1); + } + + fn release(&self) { + self.0.set(self.0.get().saturating_sub(1)); + } +} + +pub fn use_provide_overlay_claims() { + use_provide_root_context(OverlayClaims::default); +} + +pub fn use_overlay_claims() -> OverlayClaims { + consume_root_context::() +} + +pub fn use_overlay_claim() { + let claims = use_overlay_claims(); + let released = claims.clone(); + + use_hook(move || claims.acquire()); + use_drop(move || released.release()); +} + +pub fn use_overlay_claim_when(active: bool) { + let claims = use_overlay_claims(); + let held = use_hook(|| Rc::new(Cell::new(false))); + + if held.get() != active { + held.set(active); + if active { + claims.acquire(); + } else { + claims.release(); + } + } + + let released = claims; + let held_at_drop = held; + use_drop(move || { + if held_at_drop.get() { + released.release(); + } + }); +} diff --git a/packages/oneclient_app/src/layout/animated_outlet.rs b/packages/oneclient_app/src/layout/animated_outlet.rs index 4ea329e3f..a5a42a68e 100644 --- a/packages/oneclient_app/src/layout/animated_outlet.rs +++ b/packages/oneclient_app/src/layout/animated_outlet.rs @@ -2,8 +2,9 @@ use freya::animation::*; use freya::prelude::*; use freya::router::*; -use super::app_shell::{appshell_overlay, back_button, hides_overlay}; +use super::app_shell::{appshell_overlay, back_button, hides_overlay, navigate_back}; use crate::Route; +use crate::hooks::use_overlay_claims; use crate::theme; use crate::ui::entrance_motion_layer; @@ -46,6 +47,23 @@ fn is_cluster_route(route: &Route) -> bool { ) } +/// Checks if it should go back to home or somewhere else +fn escape_exits_section(route: &Route) -> bool { + is_cluster_route(route) || is_sidebar_route(route) +} + +/// Escape belongs to whatever the user is typing in before it belongs to the +/// router so a search field does not throw the page away mid-word +fn is_text_entry(role: AccessibilityRole) -> bool { + matches!( + role, + AccessibilityRole::TextInput + | AccessibilityRole::MultilineTextInput + | AccessibilityRole::SearchInput + | AccessibilityRole::PasswordInput + ) +} + fn enter_kind(from: &Route, to: &Route) -> Enter { if (is_sidebar_route(from) && is_sidebar_route(to)) || (is_cluster_route(from) && is_cluster_route(to)) @@ -66,6 +84,9 @@ impl Component for AnimatedAppOutlet { let mut router = use_animated_router::(); let route = use_route::(); + let at_home = matches!(&route, Route::Home {}); + let exits_section = escape_exits_section(&route); + let mut origin_route = use_state(|| route.clone()); let mut last_route = use_state(|| route.clone()); @@ -79,6 +100,41 @@ impl Component for AnimatedAppOutlet { let back_title = origin_route.read().title(); + let platform = Platform::get(); + let focused_node = platform.focused_accessibility_node; + let focused_id = platform.focused_accessibility_id; + + let key_claims = use_overlay_claims(); + let pointer_claims = key_claims.clone(); + + let on_global_key = move |e: Event| { + if e.key != Key::Named(NamedKey::Escape) || key_claims.any() { + return; + } + + if is_text_entry(focused_node.peek().role()) { + focused_id.peek().request_unfocus(); + return; + } + + if exits_section { + let _ = RouterContext::get().push(Route::Home {}); + } else { + navigate_back(at_home); + } + }; + + let on_global_pointer = move |e: Event| { + if pointer_claims.any() { + return; + } + match e.button() { + Some(MouseButton::Back) => navigate_back(at_home), + Some(MouseButton::Forward) => RouterContext::get().go_forward(), + _ => {} + } + }; + let anim = use_animation(|_conf| { AnimNum::new(0., 1.) .time(430) @@ -170,6 +226,8 @@ impl Component for AnimatedAppOutlet { .width(Size::fill()) .height(Size::fill()) .overflow(Overflow::Clip) + .on_global_key_down(on_global_key) + .on_global_pointer_press(on_global_pointer) .maybe_child(overlay) .child(column) } diff --git a/packages/oneclient_app/src/layout/app_shell.rs b/packages/oneclient_app/src/layout/app_shell.rs index c45c9a58d..f5df1122c 100644 --- a/packages/oneclient_app/src/layout/app_shell.rs +++ b/packages/oneclient_app/src/layout/app_shell.rs @@ -600,12 +600,22 @@ pub(crate) fn gradient_overlay_radial() -> impl IntoElement { )) } +pub(crate) fn navigate_back(at_home: bool) { + let router = RouterContext::get(); + if router.can_go_back() { + router.go_back(); + } else if !at_home { + let _ = router.push(Route::Home {}); + } +} + pub(crate) fn back_button(destination: &str) -> impl IntoElement { Button::new() .ghost() .small() .on_press(|_| { - RouterContext::get().go_back(); + // Only rendered off home so the history-empty fallback is reachable + navigate_back(false); }) .margin(Gaps::new(0., 0., 0., 32.)) .child(Icon::new(IconType::ArrowLeft).size(12.)) diff --git a/packages/oneclient_app/src/layout/root_layout.rs b/packages/oneclient_app/src/layout/root_layout.rs index 06f0c8514..092cd6ae9 100644 --- a/packages/oneclient_app/src/layout/root_layout.rs +++ b/packages/oneclient_app/src/layout/root_layout.rs @@ -6,7 +6,7 @@ use crate::components::{ NotificationCenter, PackageUpdatePopup, SplashCurtain, StatusBar, Toasts, UpdatePromptOverlay, }; -use crate::hooks::{SplashState, use_provide_splash}; +use crate::hooks::{SplashState, use_provide_overlay_claims, use_provide_splash}; use crate::layout::HomeArtPrefetch; use crate::motion::AnimationClockDriver; use crate::routes::Route; @@ -21,6 +21,7 @@ impl Component for RootLayout { let active = use_state(|| false); let home_ready = use_state(|| false); use_provide_splash(SplashState { active, home_ready }); + use_provide_overlay_claims(); // macOS rounds the window natively so Freya must not round on top of it #[cfg(target_os = "macos")] diff --git a/packages/oneclient_app/src/layout/settings_shell.rs b/packages/oneclient_app/src/layout/settings_shell.rs index 0700c104e..562d03623 100644 --- a/packages/oneclient_app/src/layout/settings_shell.rs +++ b/packages/oneclient_app/src/layout/settings_shell.rs @@ -794,7 +794,7 @@ impl Component for SidebarItem { .on_pointer_enter(move |_| *hovering.write() = true) .on_pointer_leave(move |_| *hovering.write() = false) .map(route, |el, route| { - el.on_all_press(move |_| { + el.on_press(move |_| { let _ = RouterContext::get().replace(route.clone()); }) }); diff --git a/packages/oneclient_app/src/view/app/browser/index/cards.rs b/packages/oneclient_app/src/view/app/browser/index/cards.rs index b05f09bdf..c5edbc32d 100644 --- a/packages/oneclient_app/src/view/app/browser/index/cards.rs +++ b/packages/oneclient_app/src/view/app/browser/index/cards.rs @@ -125,14 +125,13 @@ impl Component for PackageCard { .a11y_focusable(true) .a11y_role(AccessibilityRole::Button) .cursor(CursorIcon::Pointer) - .on_all_press(move |_| open_package(cluster_id, &package_type, provider, &id)) + .on_press(move |_| open_package(cluster_id, &package_type, provider, &id)) .child( rect() .margin(Gaps::new_all(1.)) .corner_radius(CornerRadius::new(10., 10., 0., 0.)) .overflow(Overflow::Clip) .child(PackageBanner::new(icon_url, BANNER_H)) - // Top-left of the banner the first thing worth knowing about a result and the opposite corner is the install button's .maybe_child(self.installed.map(|installed| { rect() .position(Position::new_absolute().top(8.).left(8.)) @@ -258,7 +257,7 @@ impl Component for ListRow { .a11y_focusable(true) .a11y_role(AccessibilityRole::Button) .cursor(CursorIcon::Pointer) - .on_all_press(move |_| open_package(cluster_id, &package_type, provider, &id)) + .on_press(move |_| open_package(cluster_id, &package_type, provider, &id)) .child(Thumbnail::new(item.icon_url.clone(), 48.).radius(8.)) .child( rect() @@ -303,7 +302,6 @@ impl Component for ListRow { ), ) .child(downloads_row(item.downloads)) - // Kept once installed rather than dropped a row that loses its button leaves its neighbours misaligned .child(InstallButton::new( &self.item, self.cluster_id, @@ -313,7 +311,6 @@ impl Component for ListRow { } } -/// Same version pick and dispatch as the install button on the package page #[derive(PartialEq)] struct InstallButton { provider: ProviderId, @@ -372,7 +369,7 @@ impl Component for InstallButton { rect() // The card behind is one big press target stop here so the package page doesn't open on top of the install - .on_all_press(|e: Event| e.stop_propagation()) + .on_press(|e: Event| e.stop_propagation()) .child( Button::new() .primary() diff --git a/packages/oneclient_app/src/view/app/browser/index/sidebar.rs b/packages/oneclient_app/src/view/app/browser/index/sidebar.rs index 66b7dfdb3..ad8146310 100644 --- a/packages/oneclient_app/src/view/app/browser/index/sidebar.rs +++ b/packages/oneclient_app/src/view/app/browser/index/sidebar.rs @@ -100,7 +100,7 @@ impl Component for CategoryRow { .cursor(CursorIcon::Pointer) .on_pointer_enter(move |_| hovering.set(true)) .on_pointer_leave(move |_| hovering.set(false)) - .on_all_press(move |_| on_toggle.call(())) + .on_press(move |_| on_toggle.call(())) .child( label() .text(capitalize(&self.name)) diff --git a/packages/oneclient_app/src/view/app/browser/package/sidebar.rs b/packages/oneclient_app/src/view/app/browser/package/sidebar.rs index 04939b5d9..628e86598 100644 --- a/packages/oneclient_app/src/view/app/browser/package/sidebar.rs +++ b/packages/oneclient_app/src/view/app/browser/package/sidebar.rs @@ -208,7 +208,7 @@ impl Component for ProviderTag { el.border(border_all_color(1., colors::brand())) }) .cursor(CursorIcon::Pointer) - .on_all_press(move |_| confirm.set(Some(url.clone()))) + .on_press(move |_| confirm.set(Some(url.clone()))) }) } } @@ -302,7 +302,7 @@ impl Component for AuthorRow { .a11y_focusable(true) .a11y_role(AccessibilityRole::Button) .cursor(CursorIcon::Pointer) - .on_all_press(move |_| confirm.set(Some(url.clone()))) + .on_press(move |_| confirm.set(Some(url.clone()))) }) .maybe(interactive && focused, |el| { el.border(border_all_color(1., colors::brand())) @@ -439,7 +439,7 @@ impl Component for LinkRow { el.border(border_all_color(1., colors::brand())) }) .cursor(CursorIcon::Pointer) - .on_all_press(move |_| confirm.set(Some(url.clone()))) + .on_press(move |_| confirm.set(Some(url.clone()))) .child( Icon::new(IconType::Link03) .size(14.) diff --git a/packages/oneclient_app/src/view/app/cluster/logs.rs b/packages/oneclient_app/src/view/app/cluster/logs.rs index 39725181f..686940f1f 100644 --- a/packages/oneclient_app/src/view/app/cluster/logs.rs +++ b/packages/oneclient_app/src/view/app/cluster/logs.rs @@ -425,7 +425,7 @@ impl Component for LogPicker { }, )) .cursor(CursorIcon::Pointer) - .on_all_press(move |e: Event| { + .on_press(move |e: Event| { e.stop_propagation(); open.toggle(); }) @@ -555,7 +555,7 @@ impl Component for PickerRow { .cursor(CursorIcon::Pointer) .on_pointer_enter(move |_| hovering.set(true)) .on_pointer_leave(move |_| hovering.set(false)) - .on_all_press(self.on_press.clone()) + .on_press(self.on_press.clone()) .child( rect() .vertical() diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs index c5b789e4c..becfcf637 100644 --- a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs +++ b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs @@ -9,7 +9,7 @@ use crate::components::{ Button, CardLayout, Icon, IconType, PackageEntry, PackageRow, ScrollArea, ScrollAreaCtx, Segment, SegmentedControl, TabBar, TabItem, TextInput, }; -use crate::hooks::use_dispatch; +use crate::hooks::{use_dispatch, use_overlay_claim}; use crate::routes::Route; use crate::theme::colors; use crate::{Actions, utils}; @@ -311,6 +311,8 @@ struct FilterPopover { impl Component for FilterPopover { fn render(&self) -> impl IntoElement { + use_overlay_claim(); + let mut sort = self.sort; let current_sort = self.current_sort; let mut enabled_filter = self.enabled_filter; @@ -473,7 +475,7 @@ impl Component for ChoiceRow { el.border(crate::ui::border_all_color(1., colors::brand())) }) .cursor(CursorIcon::Pointer) - .on_all_press(self.on_press.clone()) + .on_press(self.on_press.clone()) .child( label() .text(text)