From 21dc169578229afa3d4bf16fb82b689ff805b68f Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:59:22 +0200 Subject: [PATCH] fix(ui): status bar close button, and the layer saturation behind it --- .../src/components/navbar/mod.rs | 2 +- .../src/components/status_bar.rs | 83 ++++++++++++++----- 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/packages/oneclient_app/src/components/navbar/mod.rs b/packages/oneclient_app/src/components/navbar/mod.rs index 47e8eb07..150c3832 100644 --- a/packages/oneclient_app/src/components/navbar/mod.rs +++ b/packages/oneclient_app/src/components/navbar/mod.rs @@ -41,7 +41,7 @@ pub fn window_controls() -> impl IntoElement { .main_align(Alignment::End) .cross_align(Alignment::Center) .spacing(8.) - .layer(Layer::OverlayLevel(u8::MAX)) + .layer(Layer::OverlayLevel(15)) .child( navbar_button() .child(Icon::new(IconType::Minus)) diff --git a/packages/oneclient_app/src/components/status_bar.rs b/packages/oneclient_app/src/components/status_bar.rs index ecf4c9be..6db20068 100644 --- a/packages/oneclient_app/src/components/status_bar.rs +++ b/packages/oneclient_app/src/components/status_bar.rs @@ -10,6 +10,14 @@ use crate::theme::colors; const BAR_HEIGHT: f32 = 34.; const AMBER: Color = Color::from_rgb(191, 122, 26); +/// `Layer::RelativeOverlay(n)` resolves to `n * (i16::MAX / 16)`, so any `n` +/// past 16 pins to `i16::MAX`. Children resolve to `parent + 1`, which saturates +/// to the *same* layer — and a layer is an unordered set, so the bar's own +/// background painted over its contents about as often as not. That is what +/// made the message and the close button come and go. 14 keeps the bar above +/// every popup while leaving its children room to sit in front of it. +const BAR_LAYER: u8 = 14; + #[derive(Clone, Copy, PartialEq, Eq, Hash)] enum Issue { NoInternet, @@ -121,15 +129,31 @@ impl Component for StatusBanner { }); let p = intro.get().value(); + let mut close_hover = use_state(|| false); + + // Pressing close unmounts the banner from under the pointer, so there is + // no `leave` to put the cursor back. + use_drop(|| Cursor::set(CursorIcon::default())); + let close = issue.closeable().then(|| { rect() - .position(Position::new_absolute().right(10.)) .center() - .width(Size::px(20.)) - .height(Size::px(20.)) + .width(Size::px(22.)) + .height(Size::px(22.)) .corner_radius(CornerRadius::new_all(6.)) - .on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer)) - .on_pointer_leave(|_| Cursor::set(CursorIcon::default())) + .background(if *close_hover.read() { + Color::WHITE.with_a(46) + } else { + Color::TRANSPARENT + }) + .on_pointer_enter(move |_| { + close_hover.set(true); + Cursor::set(CursorIcon::Pointer); + }) + .on_pointer_leave(move |_| { + close_hover.set(false); + Cursor::set(CursorIcon::default()); + }) .on_press(move |_| { dismissed.write().insert(issue); }) @@ -137,21 +161,18 @@ impl Component for StatusBanner { .into_element() }); - // The icon+message row is laid out directly in the bar rather than in a - // nested auto-sized wrapper: a transparent rect with no explicit size - // measures to nothing here, which is what was swallowing the message. - rect() - .width(Size::window_percent(100.)) - .height(Size::px(BAR_HEIGHT)) - .position(Position::new_global().bottom(0.).left(0.)) - .layer(Layer::OverlayLevel(u8::MAX - 20)) - .background(issue.background()) - .opacity(p) + // The message is centred by a flex spacer on each side, and the close + // button rides the end of the trailing one. It used to be absolutely + // positioned, which pinned it to the top of the content box — inside the + // bar's horizontal padding, so it sat high and well short of the edge. + let leading = rect().width(Size::flex(1.0)).height(Size::fill()); + + let message = rect() .horizontal() - .main_align(Alignment::Center) + .height(Size::fill()) .cross_align(Alignment::Center) .spacing(8.) - .padding(Gaps::new_symmetric(0., 40.)) + .layer(Layer::OverlayLevel(u8::MAX - 20)) .child(Icon::new(issue.icon()).size(15.).color(Color::WHITE)) .child( label() @@ -160,7 +181,31 @@ impl Component for StatusBanner { .font_weight(FontWeight::MEDIUM) .max_lines(1) .color(Color::WHITE), - ) - .maybe_child(close) + ); + + let trailing = rect() + .width(Size::flex(1.0)) + .height(Size::fill()) + .horizontal() + .main_align(Alignment::End) + .cross_align(Alignment::Center) + .layer(Layer::Relative(1)) + .maybe_child(close); + + rect() + .width(Size::window_percent(100.)) + .height(Size::px(BAR_HEIGHT)) + .position(Position::new_global().bottom(0.).left(0.)) + .layer(Layer::RelativeOverlay(BAR_LAYER)) + .background(issue.background()) + .opacity(p) + .horizontal() + .content(Content::Flex) + .cross_align(Alignment::Center) + .spacing(8.) + .padding(Gaps::new_symmetric(0., 10.)) + .child(leading) + .child(message) + .child(trailing) } }