bar: entrance animation on first map

ANIMATION WORK #3. Liquid Motion only: the island's layer-shell top
margin springs (anim::spring_to) from just above its resting position
up to the theme's configured margin.top over 420ms on first map, plus
an opacity-only fade (the new bar-entrance CSS class / bar-in
keyframe). Deliberately animates the surface's own layer-shell margin
in Rust rather than a CSS margin on an inner widget: that would touch
box-model geometry, and WorkspaceTrail::place()'s single-shot initial
sample is a documented previous crash site for exactly that kind of
still-moving-on-first-paint bug (see the ws-in/row-in guard's own
comment further down). A layer-shell margin change repositions the
whole surface without touching any widget's own measured size, so it's
invisible to compute_bounds(host) by construction.

glass-workbench (flush edge-to-edge bar, no floating margin to slide
from) gets nothing. spotlight gets the opacity fade only -- its
capsule already has its own width/drawer motion in flight during a
real search, so a margin spring felt like piling onto that rather than
complementing it.
This commit is contained in:
Breadway 2026-08-26 19:35:44 +08:00
parent c0a013c8ed
commit 9761b85112
2 changed files with 63 additions and 0 deletions

View file

@ -350,6 +350,58 @@ impl SimpleComponent for App {
Exclusive::Px(px) => px, Exclusive::Px(px) => px,
}; };
root.set_exclusive_zone(exclusive_zone); root.set_exclusive_zone(exclusive_zone);
// ANIMATION WORK #3: bar entrance on first map, Liquid Motion
// only. The flush glass-workbench bar has no floating margin to
// slide from (it sits edge-to-edge against the screen), so it
// gets nothing; spotlight's capsule already has its own width/
// drawer motion in flight during a real search, so stacking a
// margin spring underneath that felt like fighting it rather than
// adding to it — it gets a plain opacity fade instead (the
// `bar-entrance` CSS class, added unconditionally by both arms
// below; only liquid-motion also gets the margin spring).
//
// This animates the SURFACE's own layer-shell top margin via
// `anim::spring_to`, not a CSS property, deliberately: `theme.rs`
// has no `margin-top` transition/keyframe reachable from here
// that wouldn't also require moving `root_vbox`'s or
// `center_box`'s own CSS margin, which — unlike an outer
// layer-shell margin, a compositor-level surface placement, not a
// GTK box-model property at all — WOULD perturb every descendant
// widget's own measured size, including the workspace row's.
// `WorkspaceTrail::place()`'s single-shot initial geometry sample
// is a documented previous crash site for exactly that: `ws-in`'s
// margin-top keyframe on a first-build button raced that sample
// and froze the trail pill low (see the `!is_first_build` guard
// and its comment, a few hundred lines below, for the full
// story). A layer-shell margin change repositions the whole
// surface on screen without touching any widget's own box model,
// so `compute_bounds(host)`'s host-local coordinates — the trail's
// whole coordinate system — never see it move at all, by
// construction rather than by luck.
let theme_id = theme::shell_theme().id().to_string();
if theme_id == "liquid-motion" {
const BAR_ENTRANCE_DROP: i32 = 18;
let target_top = window_spec.margin.top;
let start_top = (target_top - BAR_ENTRANCE_DROP).max(0);
root.set_margin(Edge::Top, start_top);
root.add_css_class("bar-entrance");
let played = Rc::new(Cell::new(false));
root.connect_map(move |win| {
// Guard against replaying on a later remap (e.g. a
// satellite hidden/reshown — see `root.set_visible(false)`
// a few lines below for the unbound-satellite case): this
// is a first-paint entrance, not a reveal animation.
if played.replace(true) {
return;
}
let target = win.clone();
bread_theme::anim::spring_to(win, start_top, target_top, 420.0, move |v| {
target.set_margin(Edge::Top, v);
});
});
} else if theme_id == "spotlight" {
root.add_css_class("bar-entrance");
}
// breadbar never called `set_keyboard_mode` before Phase 2 — it // breadbar never called `set_keyboard_mode` before Phase 2 — it
// relied on gtk4-layer-shell's own default (`KeyboardMode::None`), // relied on gtk4-layer-shell's own default (`KeyboardMode::None`),
// which is exactly what the builtin manifest's `keyboard = "none"` // which is exactly what the builtin manifest's `keyboard = "none"`

View file

@ -243,6 +243,17 @@ fn load_css() -> String {
@keyframes row-in {{ from {{ opacity: 0; margin-top: 8px; }} to {{ opacity: 1; margin-top: 0; }} }}\ @keyframes row-in {{ from {{ opacity: 0; margin-top: 8px; }} to {{ opacity: 1; margin-top: 0; }} }}\
@keyframes digit-flip {{ from {{ opacity: 0; margin-top: 7px; }} to {{ opacity: 1; margin-top: 0; }} }}\ @keyframes digit-flip {{ from {{ opacity: 0; margin-top: 7px; }} to {{ opacity: 1; margin-top: 0; }} }}\
@keyframes caret-draw {{ from {{ margin-right: 200px; opacity: 0.2; }} to {{ margin-right: 4px; opacity: 1; }} }}\ @keyframes caret-draw {{ from {{ margin-right: 200px; opacity: 0.2; }} to {{ margin-right: 4px; opacity: 1; }} }}\
/* ANIMATION WORK #3, bar entrance on first map: opacity ONLY —\
no margin/geometry term so this can never perturb any\
descendant's own box-model size (see main.rs's own long\
comment on this, next to where `bar-entrance` gets added, for\
why that matters to the workspace trail specifically).\
Liquid Motion additionally springs the surface's own\
layer-shell top margin via `anim::spring_to` in Rust, which\
this keyframe knows nothing about; glass-workbench never adds\
this class at all. */\
@keyframes bar-in {{ from {{ opacity: 0; }} }}\
.bar-entrance {{ animation: bar-in 0.4s {spring_settle} both; }}\
window.breadbar {{ background-color: alpha(@bg, {bg_alpha}); color: @on-bg;\ window.breadbar {{ background-color: alpha(@bg, {bg_alpha}); color: @on-bg;\
border-radius: {radius_bar}; {window_border}\ border-radius: {radius_bar}; {window_border}\
transition: border-radius 0.3s {spring_settle}; }}\ transition: border-radius 0.3s {spring_settle}; }}\