fix(ui): status bar close button, and the layer saturation behind it

This commit is contained in:
LynithDev 2026-07-28 13:59:22 +02:00
parent 6344d2e985
commit 21dc169578
2 changed files with 65 additions and 20 deletions

View file

@ -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))

View file

@ -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)
}
}