Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c993e53
fix(hero): clamp intro brand height to viewport width.
bengidev Aug 31, 2026
1f9405a
feat(welcome): add chrome reveal animation state.
bengidev Aug 31, 2026
8dfe7ac
feat(welcome): morph brand and fade in chrome on load.
bengidev Aug 31, 2026
8d2399b
feat(welcome): drive intro animation from desktop render loop.
bengidev Aug 31, 2026
c10d678
fix(welcome): defer intro clock until first render tick.
bengidev Aug 31, 2026
e6053fb
fix(welcome): block Enter until chrome reveal finishes.
bengidev Aug 31, 2026
065cab8
fix(welcome): disable chrome controls during intro reveal.
bengidev Aug 31, 2026
5d3ffdc
test(welcome): cover intro gating and brand morph endpoints.
bengidev Aug 31, 2026
28af8b1
fix(welcome): keep Enter CTA enabled during intro reveal.
bengidev Aug 31, 2026
a45d793
fix(welcome): keep theme toggle enabled during intro reveal.
bengidev Aug 31, 2026
9b18495
fix(hero): align welcome brand center with rendered layout.
bengidev Aug 31, 2026
a71aeb7
fix(welcome): start hero transition after home window resize.
bengidev Aug 31, 2026
f1fc45a
fix(welcome): hide static brand during hero transition morph.
bengidev Aug 31, 2026
58b51f5
fix(welcome): defer home resize until hero transition completes.
bengidev Aug 31, 2026
ccbe899
fix(hero): track rendered brand center for transition start.
bengidev Aug 31, 2026
f5c4bdf
refactor(welcome): remove hero morph transition to home.
bengidev Aug 31, 2026
be27317
change(theme): default theme mode to light.
bengidev Aug 31, 2026
3acb081
test(preferences): expect light as default theme mode.
bengidev Aug 31, 2026
cb1c97f
fix(welcome): harden intro reveal and left-align description.
bengidev Aug 31, 2026
befb01d
fix(welcome): align hero copy column with measured tagline width.
bengidev Aug 31, 2026
245a8d8
fix(welcome): address intro reveal review follow-ups.
bengidev Aug 31, 2026
410a396
style: apply rustfmt and fix clippy in welcome intro tests.
bengidev Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
366 changes: 146 additions & 220 deletions src/app/desktop.rs

Large diffs are not rendered by default.

144 changes: 118 additions & 26 deletions src/app/hero/layout.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
//! Hero layout math — welcome center, docked title-bar slot (layout A).

use super::brand::{BRAND_ASPECT, brand_width};
use crate::app::state::{HOME_WINDOW_HEIGHT, HOME_WINDOW_WIDTH};
use super::brand::BRAND_ASPECT;
use crate::app::viewport::WindowViewport;
use gpui_component::TITLE_BAR_HEIGHT;
use crate::shared::theme::TypeRole;

pub const BRAND_HERO_MIN: f32 = 220.0;
pub const BRAND_HERO_MAX: f32 = 320.0;
/// Shell title-bar brand height.
pub const BRAND_SHELL_HEIGHT: f32 = 18.0;

/// Ghost xsmall icon button width in the shell title bar.
#[allow(dead_code)]
pub const SHELL_TOGGLE_WIDTH: f32 = 28.0;
#[allow(dead_code)]
pub const SHELL_TITLE_GAP: f32 = 4.0;

const WELCOME_EDGE_INSET_H: f32 = 16.0;
const WELCOME_HEADER_BAND: f32 = 46.0;
pub const WELCOME_EDGE_INSET_H: f32 = 16.0;
pub const WELCOME_TITLEBAR_HEIGHT: f32 = 38.0;
pub const WELCOME_EDGE_INSET_TOP: f32 = 4.0;
pub const WELCOME_EDGE_INSET_BOTTOM: f32 = 20.0;
pub const WELCOME_HERO_BRAND_FRAME_EXTRA: f32 = 40.0;
pub const WELCOME_ACTION_SPACER: f32 = 60.0;
pub const WELCOME_ENTER_BUTTON_HEIGHT: f32 = 48.0;

const WELCOME_ACTION_BAND: f32 = 260.0;
const WELCOME_HEADER_GAP: f32 = 8.0;
const WELCOME_THEME_TOGGLE_HEIGHT: f32 = 32.0;

/// macOS traffic-light inset matches gpui-component `TITLE_BAR_LEFT_PADDING`.
pub fn title_bar_left_padding() -> f32 {
Expand All @@ -37,13 +46,23 @@ pub fn responsive_hero_size(available_width: f32, available_height: f32) -> f32
width_limit.min(height_limit).min(BRAND_HERO_MAX)
}

/// Center of the large welcome brand in window coordinates.
pub fn welcome_brand_center(viewport: WindowViewport) -> (f32, f32) {
let hero_height = responsive_brand_height(viewport);
let content_top = WELCOME_HEADER_BAND;
let content_height = (viewport.height - content_top - WELCOME_ACTION_BAND).max(hero_height);
let center_y = content_top + content_height * 0.5;
(viewport.width * 0.5, center_y)
/// Height of the welcome header row (title + subtitle).
pub fn welcome_header_row_height() -> f32 {
let text_column = TypeRole::LabelMd.size() * TypeRole::LabelMd.line_height()
+ 2.0
+ TypeRole::MonoSm.size() * TypeRole::MonoSm.line_height();
text_column.max(WELCOME_THEME_TOGGLE_HEIGHT)
}

/// Vertical space available for the centered brand frame and copy column.
pub fn welcome_vertical_content_budget(viewport: WindowViewport) -> f32 {
(viewport.height
- WELCOME_EDGE_INSET_TOP
- WELCOME_EDGE_INSET_BOTTOM
- welcome_header_row_height()
- WELCOME_HEADER_GAP
- WELCOME_ACTION_BAND)
.max(0.0)
}

/// Responsive welcome brand height.
Expand All @@ -55,30 +74,45 @@ pub fn responsive_brand_height(viewport: WindowViewport) -> f32 {
.min(BRAND_HERO_MAX / BRAND_ASPECT)
}

/// Center of the docked brand: `[toggle-left] [brand lockup]` (layout A).
pub fn docked_brand_center(viewport: WindowViewport) -> (f32, f32) {
let title_h = TITLE_BAR_HEIGHT.as_f32();
let width = brand_width(BRAND_SHELL_HEIGHT);
let x = title_bar_left_padding() + SHELL_TOGGLE_WIDTH + SHELL_TITLE_GAP + width * 0.5;
let _ = viewport;
(x, title_h * 0.5)
/// Large centered brand height during the show-off phase.
///
/// Unlike the old wireframe cube (square), the brand lockup is very wide
/// (`BRAND_ASPECT`), so height must be derived from available width.
pub fn show_off_brand_height(viewport: WindowViewport) -> f32 {
let hero = responsive_brand_height(viewport);
let width_limit = (viewport.width - WELCOME_EDGE_INSET_H * 2.0) / BRAND_ASPECT;
let available_height = welcome_vertical_content_budget(viewport);
// Prominent intro size that still fits the viewport; morphs down to `hero`.
width_limit.min(available_height * 0.4).max(hero)
}

/// Viewport used to compute the docked slot after welcome completes.
pub fn home_transition_viewport() -> WindowViewport {
WindowViewport {
width: HOME_WINDOW_WIDTH as f32,
height: HOME_WINDOW_HEIGHT as f32,
}
/// Linear interpolation between two values.
pub fn lerp_f32(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}

#[cfg(test)]
mod tests {
use super::*;
use crate::app::hero::brand::brand_width;
use gpui_component::TITLE_BAR_HEIGHT;

/// Center of the docked brand: `[toggle-left] [brand lockup]` (layout A).
fn docked_brand_center(viewport: WindowViewport) -> (f32, f32) {
let title_h = TITLE_BAR_HEIGHT.as_f32();
let width = brand_width(BRAND_SHELL_HEIGHT);
let x = title_bar_left_padding() + SHELL_TOGGLE_WIDTH + SHELL_TITLE_GAP + width * 0.5;
let _ = viewport;
(x, title_h * 0.5)
}

#[test]
fn docked_brand_sits_after_left_toggle() {
let (x, y) = docked_brand_center(home_transition_viewport());
let viewport = WindowViewport {
width: 1280.0,
height: 800.0,
};
let (x, y) = docked_brand_center(viewport);
let width = brand_width(BRAND_SHELL_HEIGHT);
let expected_x =
title_bar_left_padding() + SHELL_TOGGLE_WIDTH + SHELL_TITLE_GAP + width * 0.5;
Expand All @@ -91,4 +125,62 @@ mod tests {
assert_eq!(responsive_hero_size(440.0, 360.0), BRAND_HERO_MIN);
assert_eq!(responsive_hero_size(1200.0, 900.0), BRAND_HERO_MAX);
}

#[test]
fn show_off_brand_fits_within_viewport_width() {
let viewport = WindowViewport {
width: 960.0,
height: 740.0,
};
let height = show_off_brand_height(viewport);
let width = brand_width(height);
assert!(width <= viewport.width - WELCOME_EDGE_INSET_H * 2.0 + 1.0);
assert!(height >= responsive_brand_height(viewport));
}

#[test]
fn show_off_brand_fits_narrow_viewport() {
let viewport = WindowViewport {
width: 440.0,
height: 360.0,
};
let height = show_off_brand_height(viewport);
let width = brand_width(height);
assert!(width <= viewport.width - WELCOME_EDGE_INSET_H * 2.0 + 1.0);
assert!(height >= responsive_brand_height(viewport));
}

#[test]
fn show_off_brand_settles_to_resting_height() {
let viewport = WindowViewport {
width: 960.0,
height: 740.0,
};
let hero = responsive_brand_height(viewport);
let show_off = show_off_brand_height(viewport);
let settled = lerp_f32(show_off, hero, 1.0);
assert!((settled - hero).abs() < 1e-3);
}

#[test]
fn show_off_brand_fits_within_viewport_height() {
let viewport = WindowViewport {
width: 960.0,
height: 740.0,
};
let height = show_off_brand_height(viewport);
let available = welcome_vertical_content_budget(viewport);
assert!(height + WELCOME_HERO_BRAND_FRAME_EXTRA <= available + 1.0);
}

#[test]
fn show_off_brand_fits_short_viewport_height() {
let viewport = WindowViewport {
width: 960.0,
height: 500.0,
};
let height = show_off_brand_height(viewport);
let available = welcome_vertical_content_budget(viewport);
assert!(height + WELCOME_HERO_BRAND_FRAME_EXTRA <= available + 1.0);
}
}
9 changes: 5 additions & 4 deletions src/app/hero/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

mod brand;
mod layout;
mod transition;

pub use brand::{
BRAND_ASPECT, BRAND_IMAGE, BRAND_IMAGE_INVERSE, brand_width, opencore_brand_image,
};
pub use layout::{
BRAND_HERO_MAX, BRAND_HERO_MIN, BRAND_SHELL_HEIGHT, docked_brand_center,
responsive_brand_height, responsive_hero_size, title_bar_left_padding, welcome_brand_center,
BRAND_HERO_MAX, BRAND_HERO_MIN, BRAND_SHELL_HEIGHT, WELCOME_ACTION_SPACER,
WELCOME_EDGE_INSET_BOTTOM, WELCOME_EDGE_INSET_H, WELCOME_EDGE_INSET_TOP,
WELCOME_ENTER_BUTTON_HEIGHT, WELCOME_HERO_BRAND_FRAME_EXTRA, WELCOME_TITLEBAR_HEIGHT, lerp_f32,
responsive_brand_height, responsive_hero_size, show_off_brand_height, title_bar_left_padding,
welcome_vertical_content_budget,
};
pub use transition::{HERO_TRANSITION_DURATION, HeroTransition};
121 changes: 0 additions & 121 deletions src/app/hero/transition.rs

This file was deleted.

15 changes: 0 additions & 15 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,6 @@ mod tests {
assert_eq!(state.active_screen, ActiveScreen::Welcome);
}

#[test]
fn persist_welcome_completion_defers_screen_routing() {
let store = InMemoryPreferencesStore::new();
let mut state = AppState::from_preferences(AppPreferences::default());
state
.persist_welcome_completion(&store)
.expect("persist welcome completion");

assert!(state.preferences.onboarding_completed);
assert_eq!(state.active_screen, ActiveScreen::Welcome);
let intent = state.pending_window_resize.expect("resize intent recorded");
assert_eq!(intent.width, HOME_WINDOW_WIDTH);
assert_eq!(intent.height, HOME_WINDOW_HEIGHT);
}

#[test]
fn completing_onboarding_persists_and_routes_to_home() {
let store = InMemoryPreferencesStore::new();
Expand Down
32 changes: 9 additions & 23 deletions src/app/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,27 +88,6 @@ impl AppState {
Ok(())
}

/// Persists onboarding completion and queues the home resize without routing.
pub fn persist_welcome_completion<S: PreferencesStore>(
&mut self,
store: &S,
) -> Result<(), PreferencesError> {
let mut updated = self.preferences.clone();
updated.onboarding_completed = true;
store.save(&updated)?;
self.preferences = updated;
self.pending_window_resize = Some(WindowResizeIntent {
width: HOME_WINDOW_WIDTH,
height: HOME_WINDOW_HEIGHT,
});
Ok(())
}

/// Routes to home after the welcome hero transition finishes.
pub fn finish_welcome_transition(&mut self) {
self.active_screen = ActiveScreen::Home;
}

/// Applies a reducer outcome: persist and route when completed.
pub fn apply_welcome_outcome<S: PreferencesStore>(
&mut self,
Expand All @@ -118,8 +97,15 @@ impl AppState {
match outcome {
WelcomeOutcome::Pending => {}
WelcomeOutcome::Completed => {
self.persist_welcome_completion(store)?;
self.finish_welcome_transition();
let mut updated = self.preferences.clone();
updated.onboarding_completed = true;
store.save(&updated)?;
self.preferences = updated;
self.pending_window_resize = Some(WindowResizeIntent {
width: HOME_WINDOW_WIDTH,
height: HOME_WINDOW_HEIGHT,
});
self.active_screen = ActiveScreen::Home;
}
}
Ok(())
Expand Down
Loading
Loading