introduce typography system and motion module
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
/// A cubic bezier easing function like CSS `cubic-bezier` (see https://cubic-bezier.com).
|
||||
pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> impl Fn(f32) -> f32 {
|
||||
move |t: f32| {
|
||||
let one_t = 1.0 - t;
|
||||
let one_t2 = one_t * one_t;
|
||||
let t2 = t * t;
|
||||
let t3 = t2 * t;
|
||||
|
||||
// The Bezier curve function for x and y, where x0 = 0, y0 = 0, x3 = 1, y3 = 1
|
||||
let _x = 3.0 * x1 * one_t2 * t + 3.0 * x2 * one_t * t2 + t3;
|
||||
|
||||
3.0 * y1 * one_t2 * t + 3.0 * y2 * one_t * t2 + t3
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ use gpui::{
|
||||
Styled, Window, div, px, relative,
|
||||
};
|
||||
use gpui_base::Button as BaseButton;
|
||||
use theme::{ActiveTheme, bubble_radius, button_radius, plate_active, plate_hover, wash};
|
||||
use theme::{
|
||||
ActiveTheme, TextStyle, Typeset as _, bubble_radius, button_radius, plate_active, plate_hover,
|
||||
wash,
|
||||
};
|
||||
|
||||
use crate::indicator::Indicator;
|
||||
use crate::tooltip::Tooltip;
|
||||
@@ -362,9 +365,9 @@ impl RenderOnce for Button {
|
||||
.id("label")
|
||||
.justify_center()
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.text_xs().gap_1(),
|
||||
Size::Small => this.text_sm().gap_1p5(),
|
||||
_ => this.text_sm().gap_2(),
|
||||
Size::XSmall => this.text_style(TextStyle::Caption).gap_1(),
|
||||
Size::Small => this.text_style(TextStyle::Callout).gap_1p5(),
|
||||
_ => this.text_style(TextStyle::Callout).gap(px(theme::SPACE)),
|
||||
})
|
||||
.when(!self.loading, |this| {
|
||||
this.when_some(self.icon, |this, icon| {
|
||||
|
||||
+20
-21
@@ -2,16 +2,14 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyElement, App, ClickEvent, Div, FocusHandle, IntoElement,
|
||||
ParentElement, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, Window, div, px,
|
||||
transparent_black,
|
||||
AnimationExt as _, AnyElement, App, ClickEvent, Div, FocusHandle, IntoElement, ParentElement,
|
||||
Pixels, RenderOnce, SharedString, StyleRefinement, Styled, Window, div, px, transparent_black,
|
||||
};
|
||||
use gpui_base::{DialogBackdrop, DialogPopup, DialogTitle};
|
||||
use instant::Duration;
|
||||
use theme::{ActiveTheme, SCRIM_ALPHA_DARK, hairline, scrim, surface_radius, surface_shadows};
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
||||
use crate::motion::DIALOG_IN;
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::{IconName, Root, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
@@ -272,18 +270,17 @@ impl RenderOnce for Dialog {
|
||||
padding_right = pr.to_pixels(self.width.into(), window.rem_size());
|
||||
}
|
||||
|
||||
let animation = Animation::new(Duration::from_secs_f64(0.25))
|
||||
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.));
|
||||
|
||||
let backdrop = DialogBackdrop::new()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.when(self.overlay_visible, |this| {
|
||||
this.bg(scrim(SCRIM_ALPHA_DARK))
|
||||
})
|
||||
.with_animation("fade-in", animation.clone(), move |this, delta| {
|
||||
this.opacity(delta)
|
||||
});
|
||||
.with_animation(
|
||||
"fade-in",
|
||||
DIALOG_IN.animation(),
|
||||
move |this, delta| this.opacity(delta),
|
||||
);
|
||||
|
||||
let card = DialogPopup::new()
|
||||
.flex()
|
||||
@@ -373,16 +370,18 @@ impl RenderOnce for Dialog {
|
||||
.children(footer(render_ok, render_cancel, window, cx)),
|
||||
)
|
||||
})
|
||||
.with_animation("slide-down", animation, move |this, delta| {
|
||||
// Settle at the target offset; only the entrance is animated.
|
||||
let y_offset = px(30.) * (delta - 1.);
|
||||
let mut shadow = surface_shadows();
|
||||
// The shadows fade in with the slide.
|
||||
for shadow in &mut shadow {
|
||||
shadow.color.a *= delta;
|
||||
}
|
||||
this.top(y_offset).shadow(shadow)
|
||||
});
|
||||
.with_animation(
|
||||
"slide-down",
|
||||
DIALOG_IN.animation(),
|
||||
move |this, delta| {
|
||||
let y_offset = px(2.) * (delta - 1.);
|
||||
let mut shadow = surface_shadows();
|
||||
for shadow in &mut shadow {
|
||||
shadow.color.a *= delta;
|
||||
}
|
||||
this.top(y_offset).shadow(shadow)
|
||||
},
|
||||
);
|
||||
|
||||
gpui_base::Dialog::new(cx)
|
||||
.layer(layer_ix, is_topmost)
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui_base::dock::{
|
||||
PaneRef, PanelId, TabGroupContext, TabGroupRenderer,
|
||||
};
|
||||
use gpui_base::{HandleEdge, Placement, ResizeHandleContext};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT, button_radius, panel_radius};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT, TextStyle, Typeset as _, button_radius, panel_radius};
|
||||
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::resizable::{resize_handle, resize_handle_appearance};
|
||||
@@ -349,7 +349,7 @@ impl Render for DragPreview {
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.rounded(px(button_radius()))
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text)
|
||||
.text_ellipsis()
|
||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce,
|
||||
StyleRefinement, Styled, Window, div, px, relative,
|
||||
StyleRefinement, Styled, Window, div, px,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{StyledExt as _, v_flex};
|
||||
|
||||
@@ -108,8 +108,7 @@ impl RenderOnce for GroupBox {
|
||||
this.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.line_height(relative(1.))
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.font_semibold()
|
||||
.child(title),
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, App, Hsla, IntoElement, ParentElement, RenderOnce, Styled as _,
|
||||
Transformation, Window, div, ease_in_out, percentage,
|
||||
App, Hsla, IntoElement, ParentElement, RenderOnce, Styled as _, Transformation, Window, div,
|
||||
percentage,
|
||||
};
|
||||
use instant::Duration;
|
||||
|
||||
use crate::motion::{EASE_IN_OUT, MotionSpec, Painter, pulse_delta};
|
||||
use crate::{Icon, IconName, Sizable, Size};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
@@ -45,16 +46,14 @@ impl Sizable for Indicator {
|
||||
}
|
||||
|
||||
impl RenderOnce for Indicator {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let spec = MotionSpec::new(self.speed.as_millis().max(1) as u64, EASE_IN_OUT);
|
||||
let phase = pulse_delta(&spec, Painter::from(window.current_view()), cx);
|
||||
div().child(
|
||||
self.icon
|
||||
.with_size(self.size)
|
||||
.when_some(self.color, |this, color| this.text_color(color))
|
||||
.with_animation(
|
||||
"circle",
|
||||
Animation::new(self.speed).repeat().with_easing(ease_in_out),
|
||||
|this, delta| this.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
.transform(Transformation::rotate(percentage(phase))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use gpui::{
|
||||
Action, AsKeystroke, IntoElement, KeyContext, Keystroke, ParentElement as _, RenderOnce,
|
||||
StyleRefinement, Styled, Window, div, relative,
|
||||
StyleRefinement, Styled, Window, div,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::StyledExt;
|
||||
|
||||
@@ -179,8 +179,7 @@ impl RenderOnce for Kbd {
|
||||
.min_w_5()
|
||||
.text_center()
|
||||
.rounded_sm()
|
||||
.line_height(relative(1.))
|
||||
.text_xs()
|
||||
.text_style(TextStyle::Caption)
|
||||
.whitespace_normal()
|
||||
.flex_shrink_0()
|
||||
.refine_style(&self.style)
|
||||
|
||||
@@ -23,9 +23,9 @@ pub mod switch;
|
||||
pub mod tab;
|
||||
pub mod tooltip;
|
||||
|
||||
mod animation;
|
||||
mod icon;
|
||||
mod kbd;
|
||||
pub mod motion;
|
||||
mod popover;
|
||||
mod resizable;
|
||||
mod root;
|
||||
|
||||
@@ -63,7 +63,7 @@ impl RenderedText {
|
||||
/// Render the message as a GPUI Kit text view.
|
||||
pub fn element(&self, id: ElementId, _window: &Window, cx: &App) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let code_block = StyleRefinement::default().font_family(code_font_family());
|
||||
let code_block = StyleRefinement::default().font_family(theme.font_mono.clone());
|
||||
let muted_heading = theme.text_muted;
|
||||
|
||||
TextView::markdown(id, self.text.clone())
|
||||
@@ -115,13 +115,3 @@ fn is_web_url(url: &str) -> bool {
|
||||
let url = url.to_ascii_lowercase();
|
||||
url.starts_with("http://") || url.starts_with("https://")
|
||||
}
|
||||
|
||||
fn code_font_family() -> &'static str {
|
||||
if cfg!(target_os = "macos") {
|
||||
"Menlo"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"Consolas"
|
||||
} else {
|
||||
"monospace"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::{
|
||||
Styled, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{Disableable, StyledExt, h_flex};
|
||||
|
||||
@@ -90,7 +90,7 @@ impl RenderOnce for MenuItemElement {
|
||||
.group(&self.group_name)
|
||||
.gap_x_1()
|
||||
.p_1()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text)
|
||||
.relative()
|
||||
.items_center()
|
||||
|
||||
@@ -8,7 +8,7 @@ use gpui::{
|
||||
anchored, div, px, rems,
|
||||
};
|
||||
use gpui_base::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
|
||||
use theme::{ActiveTheme, Side};
|
||||
use theme::{ActiveTheme, Side, TextStyle, Typeset as _};
|
||||
|
||||
use crate::kbd::Kbd;
|
||||
use crate::menu::menu_item::MenuItemElement;
|
||||
@@ -633,7 +633,7 @@ impl PopupMenu {
|
||||
|
||||
let this = MenuItemElement::new(ix, &group_name)
|
||||
.relative()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.py_0()
|
||||
.px(INNER_PADDING)
|
||||
.rounded(options.radius)
|
||||
@@ -663,7 +663,7 @@ impl PopupMenu {
|
||||
h_flex().cursor_default().items_center().gap_x_1().child(
|
||||
div()
|
||||
.flex_1()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(label.clone()),
|
||||
@@ -686,7 +686,7 @@ impl PopupMenu {
|
||||
})
|
||||
.disabled(*disabled)
|
||||
.h(item_height)
|
||||
.gap_x_2()
|
||||
.gap_x(px(theme::SPACE))
|
||||
.children(Self::render_icon(
|
||||
has_left_icon,
|
||||
is_checked,
|
||||
@@ -718,7 +718,7 @@ impl PopupMenu {
|
||||
.min_h(item_height)
|
||||
.size_full()
|
||||
.items_center()
|
||||
.gap_x_2()
|
||||
.gap_x(px(theme::SPACE))
|
||||
.children(Self::render_icon(
|
||||
has_left_icon,
|
||||
false,
|
||||
@@ -729,7 +729,7 @@ impl PopupMenu {
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_2()
|
||||
.gap(px(theme::SPACE))
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(label.clone())
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::{
|
||||
div, px,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{InteractiveElementExt as _, StyledExt as _, h_flex, v_flex};
|
||||
|
||||
@@ -190,8 +190,8 @@ impl RenderOnce for MessageRow {
|
||||
.when(show_author, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.gap(px(theme::SPACE))
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text_dim)
|
||||
.when_some(author, |this, author| {
|
||||
this.child(div().font_semibold().child(author))
|
||||
@@ -280,7 +280,7 @@ impl RenderOnce for WelcomeMessage {
|
||||
v_flex()
|
||||
.id(id)
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.gap(px(theme::SPACE))
|
||||
.p_3()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
@@ -295,8 +295,7 @@ impl RenderOnce for WelcomeMessage {
|
||||
.when_some(title, |this, title| {
|
||||
this.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_style(TextStyle::Headline)
|
||||
.text_color(cx.theme().text)
|
||||
.child(title),
|
||||
)
|
||||
@@ -304,7 +303,7 @@ impl RenderOnce for WelcomeMessage {
|
||||
.when_some(message, |this, message| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.child(message),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
//! The motion kit, ported from bezel (`crates/motion/src/lib.rs` and its
|
||||
//! `app.rs`, MIT License, Copyright (c) 2026 bezel contributors). TranslateY
|
||||
//! rides a relative `top` inset, which taffy applies after layout, so
|
||||
//! siblings never move.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationElement, AnimationExt as _, App, Context, ElementId, EntityId, Global,
|
||||
Hsla, IntoElement, Rgba, SharedString, Styled, Window, px,
|
||||
};
|
||||
use instant::Instant;
|
||||
|
||||
/// Redraw rate for anything driven by [`pulse_delta`].
|
||||
const PULSE_FPS: f32 = 30.0;
|
||||
|
||||
/// How long a view stays on the tick list after its last pulse paint.
|
||||
const PULSE_LEASE: Duration = Duration::from_millis(300);
|
||||
|
||||
/// Redraw rate for hover fades; [`HOVER_FADE`] paints in nine steps at it.
|
||||
const HOVER_FPS: f32 = 60.0;
|
||||
|
||||
/// Floor on how long the clock sleeps between wake-ups.
|
||||
const MIN_SLEEP: Duration = Duration::from_millis(1);
|
||||
|
||||
/// One view's claim on the clock.
|
||||
struct Lease {
|
||||
/// The fastest rate anything on this view has claimed.
|
||||
period: Duration,
|
||||
/// When this view is next owed a redraw.
|
||||
due: Instant,
|
||||
/// A notify is out and the render it provoked has not renewed yet.
|
||||
in_flight: bool,
|
||||
/// When the claim lapses if nothing renews it.
|
||||
until: Instant,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PulseClock {
|
||||
epoch: Option<Instant>,
|
||||
leases: HashMap<EntityId, Lease>,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
impl Global for PulseClock {}
|
||||
|
||||
impl PulseClock {
|
||||
fn next_wake(&self) -> Option<Instant> {
|
||||
self.leases
|
||||
.values()
|
||||
.map(|lease| lease.due.min(lease.until))
|
||||
.min()
|
||||
}
|
||||
}
|
||||
|
||||
/// A component's line back to the view that paints it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Painter(EntityId);
|
||||
|
||||
impl Painter {
|
||||
/// The view this render belongs to; take it once where the state is built.
|
||||
pub fn of<T: 'static>(cx: &Context<T>) -> Self {
|
||||
Self(cx.entity_id())
|
||||
}
|
||||
|
||||
/// Redraw this view once.
|
||||
pub fn notify(self, cx: &mut App) {
|
||||
cx.notify(self.0);
|
||||
}
|
||||
|
||||
/// Claim `fps` redraws a second; renew from `render` so an unmounted
|
||||
/// element drops off.
|
||||
pub fn lease(self, fps: f32, until: Duration, cx: &mut App) {
|
||||
lease(self.0, fps, until, cx);
|
||||
}
|
||||
|
||||
/// Whether the clock is what asked for the render running now.
|
||||
pub fn woken(self, cx: &App) -> bool {
|
||||
cx.try_global::<PulseClock>()
|
||||
.and_then(|clock| clock.leases.get(&self.0))
|
||||
.is_some_and(|lease| lease.in_flight)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EntityId> for Painter {
|
||||
fn from(entity_id: EntityId) -> Self {
|
||||
Self(entity_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Painter> for EntityId {
|
||||
fn from(painter: Painter) -> Self {
|
||||
painter.0
|
||||
}
|
||||
}
|
||||
|
||||
fn lease(view: EntityId, fps: f32, until: Duration, cx: &mut App) {
|
||||
if cx.pause_when_inactive() && cx.active_window().is_none() {
|
||||
return;
|
||||
}
|
||||
let now = cx.background_executor().now();
|
||||
let period = Duration::from_secs_f32(1.0 / fps.clamp(1.0, 240.0));
|
||||
let clock = cx.default_global::<PulseClock>();
|
||||
clock
|
||||
.leases
|
||||
.entry(view)
|
||||
.and_modify(|lease| {
|
||||
lease.period = lease.period.min(period);
|
||||
lease.due = lease.due.min(now + period);
|
||||
lease.in_flight = false;
|
||||
lease.until = lease.until.max(now + until);
|
||||
})
|
||||
.or_insert(Lease {
|
||||
period,
|
||||
due: now + period,
|
||||
in_flight: false,
|
||||
until: now + until,
|
||||
});
|
||||
if clock.running {
|
||||
return;
|
||||
}
|
||||
clock.running = true;
|
||||
cx.spawn(async move |cx| {
|
||||
loop {
|
||||
let sleep = cx.update(|cx| {
|
||||
let now = cx.background_executor().now();
|
||||
let clock = cx.default_global::<PulseClock>();
|
||||
clock
|
||||
.next_wake()
|
||||
.map(|wake| wake.saturating_duration_since(now).max(MIN_SLEEP))
|
||||
});
|
||||
let Some(sleep) = sleep else { break };
|
||||
cx.background_executor().timer(sleep).await;
|
||||
let parked = cx.update(|cx| {
|
||||
let now = cx.background_executor().now();
|
||||
tick_hover_fades();
|
||||
let clock = cx.default_global::<PulseClock>();
|
||||
clock.leases.retain(|_, lease| lease.until > now);
|
||||
if clock.leases.is_empty() {
|
||||
clock.running = false;
|
||||
return true;
|
||||
}
|
||||
let mut owed = Vec::new();
|
||||
for (view, lease) in clock.leases.iter_mut() {
|
||||
if lease.due > now {
|
||||
continue;
|
||||
}
|
||||
lease.due += lease.period;
|
||||
if lease.due <= now {
|
||||
lease.due = now + lease.period;
|
||||
}
|
||||
lease.in_flight = true;
|
||||
owed.push(*view);
|
||||
}
|
||||
for view in owed {
|
||||
cx.notify(view);
|
||||
}
|
||||
false
|
||||
});
|
||||
if parked {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Current phase `[0,1)` of a repeating spec; reduced motion returns a
|
||||
/// static 0 and schedules nothing.
|
||||
pub fn pulse_delta(spec: &MotionSpec, painter: Painter, cx: &mut App) -> f32 {
|
||||
if cx.reduced_motion() {
|
||||
return 0.0;
|
||||
}
|
||||
painter.lease(PULSE_FPS, PULSE_LEASE, cx);
|
||||
let now = cx.background_executor().now();
|
||||
let clock = cx.default_global::<PulseClock>();
|
||||
let epoch = *clock.epoch.get_or_insert(now);
|
||||
let period = spec.total().as_secs_f32();
|
||||
((now - epoch).as_secs_f32() / period).fract()
|
||||
}
|
||||
|
||||
/// A CSS `cubic-bezier(x1, y1, x2, y2)` timing function, evaluated exactly.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CubicBezier {
|
||||
pub x1: f32,
|
||||
pub y1: f32,
|
||||
pub x2: f32,
|
||||
pub y2: f32,
|
||||
}
|
||||
|
||||
impl CubicBezier {
|
||||
pub const fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
|
||||
Self { x1, y1, x2, y2 }
|
||||
}
|
||||
|
||||
fn coefficients(a: f32, b: f32) -> (f32, f32, f32) {
|
||||
let c = 3.0 * a;
|
||||
let bb = 3.0 * (b - a) - c;
|
||||
let aa = 1.0 - c - bb;
|
||||
(aa, bb, c)
|
||||
}
|
||||
|
||||
fn sample_x(&self, t: f32) -> f32 {
|
||||
let (a, b, c) = Self::coefficients(self.x1, self.x2);
|
||||
((a * t + b) * t + c) * t
|
||||
}
|
||||
|
||||
fn sample_y(&self, t: f32) -> f32 {
|
||||
let (a, b, c) = Self::coefficients(self.y1, self.y2);
|
||||
((a * t + b) * t + c) * t
|
||||
}
|
||||
|
||||
fn sample_x_derivative(&self, t: f32) -> f32 {
|
||||
let (a, b, c) = Self::coefficients(self.x1, self.x2);
|
||||
(3.0 * a * t + 2.0 * b) * t + c
|
||||
}
|
||||
|
||||
fn solve_t_for_x(&self, x: f32) -> f32 {
|
||||
let mut t = x;
|
||||
for _ in 0..8 {
|
||||
let err = self.sample_x(t) - x;
|
||||
if err.abs() < 1e-6 {
|
||||
return t;
|
||||
}
|
||||
let d = self.sample_x_derivative(t);
|
||||
if d.abs() < 1e-6 {
|
||||
break;
|
||||
}
|
||||
t -= err / d;
|
||||
}
|
||||
let (mut lo, mut hi) = (0.0_f32, 1.0_f32);
|
||||
for _ in 0..32 {
|
||||
let mid = (lo + hi) / 2.0;
|
||||
if self.sample_x(mid) < x {
|
||||
lo = mid
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
(lo + hi) / 2.0
|
||||
}
|
||||
|
||||
/// Eased output for input progress `x ∈ [0,1]`, clamped hard — gpui's
|
||||
/// animation element asserts its delta is in `[0,1]` and aborts.
|
||||
pub fn eval(&self, x: f32) -> f32 {
|
||||
if x <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
if x >= 1.0 {
|
||||
return 1.0;
|
||||
}
|
||||
self.sample_y(self.solve_t_for_x(x)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// This curve as a gpui easing closure.
|
||||
pub fn easing(self) -> impl Fn(f32) -> f32 + 'static {
|
||||
move |x| self.eval(x)
|
||||
}
|
||||
}
|
||||
|
||||
/// The signature entrance curve — CSS `cubic-bezier(0.16, 1, 0.3, 1)`.
|
||||
pub const EASE_OUT_EXPO: CubicBezier = CubicBezier::new(0.16, 1.0, 0.3, 1.0);
|
||||
/// CSS `ease-out` — width/height transitions.
|
||||
pub const EASE_OUT: CubicBezier = CubicBezier::new(0.0, 0.0, 0.58, 1.0);
|
||||
/// CSS `ease` — quick fades, menu/dialog pops.
|
||||
pub const EASE: CubicBezier = CubicBezier::new(0.25, 0.1, 0.25, 1.0);
|
||||
/// Sidebar resort glide — CSS `cubic-bezier(0.22, 1, 0.36, 1)`.
|
||||
pub const EASE_RESORT: CubicBezier = CubicBezier::new(0.22, 1.0, 0.36, 1.0);
|
||||
/// CSS `ease-in-out`.
|
||||
pub const EASE_IN_OUT: CubicBezier = CubicBezier::new(0.42, 0.0, 0.58, 1.0);
|
||||
/// Tailwind's default transition curve — CSS `cubic-bezier(0.4, 0, 0.2, 1)`.
|
||||
pub const EASE_TAILWIND: CubicBezier = CubicBezier::new(0.4, 0.0, 0.2, 1.0);
|
||||
|
||||
/// One catalog entry: duration + optional delay + curve; gpui has no native
|
||||
/// delay, so `delay_ms` holds progress at 0 until it elapses.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MotionSpec {
|
||||
pub duration_ms: u64,
|
||||
pub delay_ms: u64,
|
||||
pub curve: CubicBezier,
|
||||
}
|
||||
|
||||
impl MotionSpec {
|
||||
pub const fn new(duration_ms: u64, curve: CubicBezier) -> Self {
|
||||
Self {
|
||||
duration_ms,
|
||||
delay_ms: 0,
|
||||
curve,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn with_delay(mut self, delay_ms: u64) -> Self {
|
||||
self.delay_ms = delay_ms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wall-clock span of the whole timeline (delay + duration).
|
||||
pub fn total(&self) -> Duration {
|
||||
Duration::from_millis(self.delay_ms + self.duration_ms)
|
||||
}
|
||||
|
||||
/// Eased progress (0..1) for a raw timeline delta (0..1 across `total`).
|
||||
pub fn progress(&self, raw_delta: f32) -> f32 {
|
||||
let total = (self.delay_ms + self.duration_ms) as f32;
|
||||
if total <= 0.0 || self.duration_ms == 0 {
|
||||
return 1.0;
|
||||
}
|
||||
let t =
|
||||
(raw_delta.clamp(0.0, 1.0) * total - self.delay_ms as f32) / self.duration_ms as f32;
|
||||
self.curve.eval(t.clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
/// A oneshot gpui [`Animation`] for this spec.
|
||||
pub fn animation(&self) -> Animation {
|
||||
let spec = *self;
|
||||
Animation::new(spec.total().mul_f32(speed_scale())).with_easing(move |d| spec.progress(d))
|
||||
}
|
||||
}
|
||||
|
||||
/// Entrances: 0.5s expo-out fade + 4px rise.
|
||||
pub const FADE_IN: MotionSpec = MotionSpec::new(500, EASE_OUT_EXPO);
|
||||
/// Quick fade: 0.15s.
|
||||
pub const FADE_QUICK: MotionSpec = MotionSpec::new(150, EASE);
|
||||
/// Popover-in: 0.14s.
|
||||
pub const MENU_IN: MotionSpec = MotionSpec::new(140, EASE);
|
||||
/// Popover-out: 0.1s — quicker than the entrance.
|
||||
pub const MENU_OUT: MotionSpec = MotionSpec::new(100, EASE);
|
||||
/// Dialog-in: 0.18s.
|
||||
pub const DIALOG_IN: MotionSpec = MotionSpec::new(180, EASE);
|
||||
/// Boot-splash exit: 0.5s fade + 6px lift after a 0.15s hold.
|
||||
pub const SPLASH_OUT: MotionSpec = MotionSpec::new(500, EASE).with_delay(150);
|
||||
/// Sidebar / pane width+height transitions: 200ms ease-out.
|
||||
pub const RESIZE: MotionSpec = MotionSpec::new(200, EASE_OUT);
|
||||
/// Tab drag-reorder sliding transforms: 150ms.
|
||||
pub const TAB_SLIDE: MotionSpec = MotionSpec::new(150, EASE_OUT);
|
||||
/// Per-pane collapse: 180ms height.
|
||||
pub const COLLAPSE: MotionSpec = MotionSpec::new(180, EASE_OUT);
|
||||
/// A gliding move the reader caused rather than one they are dragging.
|
||||
pub const LAYOUT: MotionSpec = MotionSpec::new(200, EASE_OUT);
|
||||
/// Chevron rotate: 200ms.
|
||||
pub const CHEVRON: MotionSpec = MotionSpec::new(200, EASE);
|
||||
/// Scroll-to-row glide: 500ms ease-in-out.
|
||||
pub const SCROLL_GLIDE: MotionSpec = MotionSpec::new(500, EASE_IN_OUT);
|
||||
/// CSS `transition-colors` default: 150ms over [`EASE_TAILWIND`].
|
||||
pub const HOVER_FADE: MotionSpec = MotionSpec::new(150, EASE_TAILWIND);
|
||||
/// Toast slide/fade, coop's own: 250ms over [`EASE_TAILWIND`].
|
||||
pub const NOTIFICATION_IN: MotionSpec = MotionSpec::new(250, EASE_TAILWIND);
|
||||
/// Pulse loader period: 2.4s.
|
||||
pub const PULSE: MotionSpec = MotionSpec::new(2400, EASE);
|
||||
/// Spinner wave period: 750ms.
|
||||
pub const GRADIENT_SPIN: MotionSpec = MotionSpec::new(750, EASE);
|
||||
|
||||
/// Standard entrance: opacity 0→1 + translateY 4→0 over [`FADE_IN`].
|
||||
pub fn fade_in<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, FADE_IN.animation(), |el, t| {
|
||||
el.relative().opacity(t).top(px(4.0 * (1.0 - t)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Quick opacity-only fade over [`FADE_QUICK`].
|
||||
pub fn fade_quick<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, FADE_QUICK.animation(), |el, t| el.opacity(t))
|
||||
}
|
||||
|
||||
/// Popover entrance: fade + translateY −2→0 over [`MENU_IN`].
|
||||
pub fn menu_in<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, MENU_IN.animation(), |el, t| {
|
||||
el.relative()
|
||||
.opacity(0.3 + 0.7 * t)
|
||||
.top(px(-2.0 * (1.0 - t)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Popover exit over [`MENU_OUT`]; the caller supplies progress because
|
||||
/// `with_animation` replays from 0 on remount.
|
||||
pub fn menu_out<E>(id: impl Into<ElementId>, t: f32, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, MENU_OUT.animation(), move |el, _| {
|
||||
el.relative().opacity(1.0 - t).top(px(-2.0 * t))
|
||||
})
|
||||
}
|
||||
|
||||
/// Dialog entrance over [`DIALOG_IN`] (fade + 2px rise).
|
||||
pub fn dialog_in<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, DIALOG_IN.animation(), |el, t| {
|
||||
el.relative().opacity(t).top(px(2.0 * (1.0 - t)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Boot-splash exit: hold 150ms, then fade out + lift 6px over 500ms.
|
||||
pub fn splash_out<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, SPLASH_OUT.animation(), |el, t| {
|
||||
el.opacity(1.0 - t).top(px(-6.0 * t))
|
||||
})
|
||||
}
|
||||
|
||||
/// Linear interpolation.
|
||||
pub fn lerp(from: f32, to: f32, t: f32) -> f32 {
|
||||
from + (to - from) * t
|
||||
}
|
||||
|
||||
/// One element's hover fade progress over [`HOVER_FADE`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FadeEntry {
|
||||
origin: f32,
|
||||
target: f32,
|
||||
started: Instant,
|
||||
seen: u64,
|
||||
}
|
||||
|
||||
impl FadeEntry {
|
||||
fn value(&self, now: Instant, duration: Duration) -> f32 {
|
||||
let elapsed = now.saturating_duration_since(self.started);
|
||||
if duration.is_zero() || elapsed >= duration {
|
||||
return self.target;
|
||||
}
|
||||
let raw = elapsed.as_secs_f32() / duration.as_secs_f32();
|
||||
lerp(self.origin, self.target, HOVER_FADE.curve.eval(raw))
|
||||
}
|
||||
|
||||
fn settled(&self, now: Instant, duration: Duration) -> bool {
|
||||
self.origin == self.target || now.saturating_duration_since(self.started) >= duration
|
||||
}
|
||||
}
|
||||
|
||||
/// Which fade: the view that paints it plus the element key — two views
|
||||
/// using the same string would trade washes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Fade {
|
||||
pub painter: Painter,
|
||||
pub key: SharedString,
|
||||
}
|
||||
|
||||
impl Fade {
|
||||
pub fn new(painter: Painter, key: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
painter,
|
||||
key: key.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-fade progress store.
|
||||
#[derive(Default)]
|
||||
pub struct HoverFades {
|
||||
pub entries: HashMap<Fade, FadeEntry>,
|
||||
frame: u64,
|
||||
}
|
||||
|
||||
impl HoverFades {
|
||||
pub fn duration() -> Duration {
|
||||
HOVER_FADE.total().mul_f32(speed_scale())
|
||||
}
|
||||
|
||||
/// Pointer entered (`hovered`) or left the element behind `fade`.
|
||||
pub fn set_at(&mut self, fade: &Fade, hovered: bool, reduced: bool, now: Instant) {
|
||||
let target = if hovered { 1.0 } else { 0.0 };
|
||||
let duration = Self::duration();
|
||||
let current = self
|
||||
.entries
|
||||
.get(fade)
|
||||
.map(|e| e.value(now, duration))
|
||||
.unwrap_or(0.0);
|
||||
if target == 0.0 && !self.entries.contains_key(fade) {
|
||||
return;
|
||||
}
|
||||
let origin = if reduced { target } else { current };
|
||||
let seen = self.frame;
|
||||
self.entries.insert(
|
||||
fade.clone(),
|
||||
FadeEntry {
|
||||
origin,
|
||||
target,
|
||||
started: now,
|
||||
seen,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Hover progress (0..1) for `fade` at `now`; stamps liveness.
|
||||
pub fn value_at(&mut self, fade: &Fade, now: Instant) -> f32 {
|
||||
let frame = self.frame;
|
||||
match self.entries.get_mut(fade) {
|
||||
Some(entry) => {
|
||||
entry.seen = frame;
|
||||
entry.value(now, Self::duration())
|
||||
}
|
||||
None => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the frame counter and prune settled or unmounted entries.
|
||||
pub fn tick_at(&mut self, now: Instant) -> bool {
|
||||
self.frame += 1;
|
||||
let frame = self.frame;
|
||||
let duration = Self::duration();
|
||||
let mut active = false;
|
||||
self.entries.retain(|_, entry| {
|
||||
if entry.seen + 1 < frame {
|
||||
return false;
|
||||
}
|
||||
let settled = entry.settled(now, duration);
|
||||
if !settled {
|
||||
active = true;
|
||||
}
|
||||
!(settled && entry.target == 0.0)
|
||||
});
|
||||
active
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static HOVER_FADES: RefCell<HoverFades> = RefCell::new(HoverFades::default());
|
||||
}
|
||||
|
||||
/// Hover progress (0..1) for `fade` this frame.
|
||||
pub fn hover_t(fade: &Fade) -> f32 {
|
||||
HOVER_FADES.with(|fades| fades.borrow_mut().value_at(fade, Instant::now()))
|
||||
}
|
||||
|
||||
/// Record a hover flip for `fade`; prefer [`hover_listener`].
|
||||
pub fn set_hover(fade: &Fade, hovered: bool, reduced: bool) {
|
||||
HOVER_FADES.with(|fades| {
|
||||
fades
|
||||
.borrow_mut()
|
||||
.set_at(fade, hovered, reduced, Instant::now())
|
||||
});
|
||||
}
|
||||
|
||||
/// An `.on_hover` listener driving the fade.
|
||||
pub fn hover_listener(fade: Fade) -> impl Fn(&bool, &mut Window, &mut App) + 'static {
|
||||
move |hovered, _window, cx| {
|
||||
set_hover(&fade, *hovered, cx.reduced_motion());
|
||||
fade.painter.lease(HOVER_FPS, HoverFades::duration(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Once-per-tick bookkeeping for the fade store, driven by the clock.
|
||||
fn tick_hover_fades() {
|
||||
HOVER_FADES.with(|fades| {
|
||||
fades.borrow_mut().tick_at(Instant::now());
|
||||
});
|
||||
}
|
||||
|
||||
/// Blend two colors by `t` the way the browser transitions them: sRGB with
|
||||
/// premultiplied alpha, so a wash fading in never passes through grey.
|
||||
pub fn mix(from: Hsla, to: Hsla, t: f32) -> Hsla {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
if t <= 0.0 {
|
||||
return from;
|
||||
}
|
||||
if t >= 1.0 {
|
||||
return to;
|
||||
}
|
||||
let (f, g) = (Rgba::from(from), Rgba::from(to));
|
||||
let a = lerp(f.a, g.a, t);
|
||||
if a <= f32::EPSILON {
|
||||
return Hsla::from(Rgba { a: 0.0, ..g });
|
||||
}
|
||||
Hsla::from(Rgba {
|
||||
r: lerp(f.r * f.a, g.r * g.a, t) / a,
|
||||
g: lerp(f.g * f.a, g.g * g.a, t) / a,
|
||||
b: lerp(f.b * f.a, g.b * g.a, t) / a,
|
||||
a,
|
||||
})
|
||||
}
|
||||
|
||||
/// The standard hover blend: rest → hover color at this fade's progress.
|
||||
pub fn hover_blend(fade: &Fade, rest: Hsla, hover: Hsla) -> Hsla {
|
||||
mix(rest, hover, hover_t(fade))
|
||||
}
|
||||
|
||||
/// Process-wide motion speed, as raw `f32` bits; an atomic mirror because the
|
||||
/// timelines are read from free functions with no `cx` in scope.
|
||||
static SPEED: AtomicU32 = AtomicU32::new(1.0f32.to_bits());
|
||||
|
||||
/// How far every timeline in the catalog is stretched. `1.0` is the designed
|
||||
/// speed.
|
||||
pub fn speed_scale() -> f32 {
|
||||
f32::from_bits(SPEED.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Stretch every catalog timeline by `scale`; clamped, and a non-finite
|
||||
/// `scale` resets to `1.0`.
|
||||
pub fn set_speed(scale: f32) {
|
||||
let scale = if scale.is_finite() {
|
||||
scale.clamp(0.01, 100.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
SPEED.store(scale.to_bits(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Process-wide motion switches.
|
||||
#[derive(Clone, Copy)]
|
||||
struct MotionSettings {
|
||||
reduced_motion: bool,
|
||||
pause_when_inactive: bool,
|
||||
}
|
||||
|
||||
impl Global for MotionSettings {}
|
||||
|
||||
impl MotionSettings {
|
||||
/// Defaults: the OS flag rules reduce motion; the clock pauses when no
|
||||
/// window is active.
|
||||
fn get(cx: &App) -> Self {
|
||||
cx.try_global::<MotionSettings>().copied().unwrap_or(Self {
|
||||
reduced_motion: false,
|
||||
pause_when_inactive: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// App-level motion switches.
|
||||
pub trait AppExt {
|
||||
/// The OS reduce-motion flag ORed with the manual override.
|
||||
fn reduced_motion(&self) -> bool;
|
||||
|
||||
/// Set the manual reduce-motion override.
|
||||
fn set_reduced_motion(&mut self, reduced: bool);
|
||||
|
||||
/// Whether the leased clock claims frames while no window is active.
|
||||
fn pause_when_inactive(&self) -> bool;
|
||||
|
||||
fn set_pause_when_inactive(&mut self, pause: bool);
|
||||
}
|
||||
|
||||
impl AppExt for App {
|
||||
fn reduced_motion(&self) -> bool {
|
||||
self.reduce_motion() || MotionSettings::get(self).reduced_motion
|
||||
}
|
||||
|
||||
fn set_reduced_motion(&mut self, reduced: bool) {
|
||||
let mut settings = MotionSettings::get(self);
|
||||
settings.reduced_motion = reduced;
|
||||
self.set_global(settings);
|
||||
}
|
||||
|
||||
fn pause_when_inactive(&self) -> bool {
|
||||
MotionSettings::get(self).pause_when_inactive
|
||||
}
|
||||
|
||||
fn set_pause_when_inactive(&mut self, pause: bool) {
|
||||
let mut settings = MotionSettings::get(self);
|
||||
settings.pause_when_inactive = pause;
|
||||
self.set_global(settings);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||
Styled, Window, div, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{Selectable, StyledExt, h_flex};
|
||||
|
||||
@@ -98,7 +98,7 @@ impl RenderOnce for NavItem {
|
||||
.px_2()
|
||||
.py_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.gap(px(theme::SPACE))
|
||||
.rounded(px(theme::button_radius()))
|
||||
.text_color(text_color)
|
||||
.child(self.icon)
|
||||
@@ -107,7 +107,7 @@ impl RenderOnce for NavItem {
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.child(self.label),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
|
||||
@@ -5,19 +5,19 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Anchor, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context,
|
||||
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
|
||||
Subscription, Window, div, px, relative,
|
||||
Anchor, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, DismissEvent,
|
||||
ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||
Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription,
|
||||
Window, div, px, relative,
|
||||
};
|
||||
use gpui_base::{
|
||||
Toast as BaseToast, ToastManager, ToastMotion, ToastOptions, ToastStack, ToastStackState,
|
||||
ToastTransitionStatus,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::motion::NOTIFICATION_IN;
|
||||
use crate::{Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
|
||||
|
||||
/// How often the notification lifecycle clock is sampled.
|
||||
@@ -312,7 +312,7 @@ impl Render for Notification {
|
||||
this.shadow(theme::surface_shadows())
|
||||
})
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.gap(px(theme::SPACE))
|
||||
.justify_start()
|
||||
.items_start()
|
||||
.when(only_message, |this| this.items_center())
|
||||
@@ -326,12 +326,12 @@ impl Render for Notification {
|
||||
.gap_1()
|
||||
.overflow_hidden()
|
||||
.when_some(self.title.clone(), |this, title| {
|
||||
this.child(h_flex().h_5().text_sm().font_semibold().child(title))
|
||||
this.child(h_flex().h_5().text_style(TextStyle::Headline).child(title))
|
||||
})
|
||||
.when_some(self.message.clone(), |this, message| {
|
||||
this.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.when(has_title, |this| this.text_color(cx.theme().text_muted))
|
||||
.line_height(relative(1.3))
|
||||
.child(message),
|
||||
@@ -339,7 +339,7 @@ impl Render for Notification {
|
||||
})
|
||||
.when_some(content, |this, content| this.child(content))
|
||||
.when_some(action, |this, action| {
|
||||
this.gap_2().child(
|
||||
this.gap(px(theme::SPACE)).child(
|
||||
h_flex()
|
||||
.mt_2()
|
||||
.w_full()
|
||||
@@ -379,8 +379,7 @@ impl Render for Notification {
|
||||
}))
|
||||
.with_animation(
|
||||
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
||||
Animation::new(Duration::from_secs_f64(0.25))
|
||||
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||
NOTIFICATION_IN.animation(),
|
||||
move |this, delta| {
|
||||
if closing {
|
||||
let opacity = 1. - delta;
|
||||
|
||||
@@ -11,7 +11,7 @@ use gpui::{
|
||||
use gpui_base::TextSelectionLayer;
|
||||
use theme::{
|
||||
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
|
||||
CLIENT_SIDE_DECORATION_SHADOW, surface_shadows,
|
||||
CLIENT_SIDE_DECORATION_SHADOW, base_text_size, surface_shadows,
|
||||
};
|
||||
|
||||
use crate::dialog::Dialog;
|
||||
@@ -234,8 +234,8 @@ impl Root {
|
||||
|
||||
impl Render for Root {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let rem_size = cx.theme().font_size;
|
||||
let font_family = cx.theme().font_family.clone();
|
||||
let rem_size = px(base_text_size());
|
||||
let font_family = cx.theme().font_sans.clone();
|
||||
let decorations = window.window_decorations();
|
||||
|
||||
// Set the base font size
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{App, Div, Pixels, Refineable, StyleRefinement, Styled, div, px};
|
||||
pub use gpui_base::component_traits::{Disableable, Selectable};
|
||||
use theme::{ActiveTheme, surface_radius, surface_shadows};
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _, surface_radius, surface_shadows};
|
||||
|
||||
/// Returns a `Div` as horizontal flex layout.
|
||||
pub fn h_flex() -> Div {
|
||||
@@ -135,10 +135,10 @@ impl<T: Styled> StyleSized<T> for T {
|
||||
|
||||
fn input_h(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::XSmall => self.h_6().text_xs(),
|
||||
Size::Small => self.h_8().text_sm(),
|
||||
Size::Medium => self.h_9().text_base(),
|
||||
Size::Large => self.h_12().text_lg(),
|
||||
Size::XSmall => self.h_6().text_style(TextStyle::Caption),
|
||||
Size::Small => self.h_8().text_style(TextStyle::Callout),
|
||||
Size::Medium => self.h_9().text_style(TextStyle::Body),
|
||||
Size::Large => self.h_12().text_style(TextStyle::Title3),
|
||||
Size::Size(size) => self.h(px(24.)).text_size(size),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{
|
||||
div, px, transparent_black,
|
||||
};
|
||||
use gpui_base::{Spring, Switch as BaseSwitch, SwitchThumb, SwitchTrack, spring};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
||||
|
||||
@@ -113,7 +113,12 @@ impl RenderOnce for Switch {
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.when_some(label, |this, label| {
|
||||
this.child(div().text_sm().text_color(cx.theme().text).child(label))
|
||||
this.child(
|
||||
div()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text)
|
||||
.child(label),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
SwitchTrack::new((self.id.clone(), "track"))
|
||||
@@ -142,7 +147,7 @@ impl RenderOnce for Switch {
|
||||
this.child(
|
||||
div()
|
||||
.pr_3()
|
||||
.text_xs()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(description),
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::{
|
||||
AnyElement, App, ClickEvent, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{Selectable, h_flex};
|
||||
|
||||
@@ -170,7 +170,7 @@ impl RenderOnce for Tab {
|
||||
.items_center()
|
||||
.text_color(foreground)
|
||||
.when(segmented, |this| {
|
||||
this.text_xs()
|
||||
this.text_style(TextStyle::Caption)
|
||||
.flex_1()
|
||||
.h_6()
|
||||
.rounded(px(theme::button_radius()))
|
||||
@@ -188,7 +188,7 @@ impl RenderOnce for Tab {
|
||||
.h_7()
|
||||
.gap_1()
|
||||
.px_1p5()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.rounded(px(theme::button_radius()))
|
||||
.overflow_hidden()
|
||||
.when(selected && !disabled, |this| {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Window, px, relative,
|
||||
Window, px,
|
||||
};
|
||||
use gpui_base::Tooltip as BaseTooltip;
|
||||
use theme::{ActiveTheme, control_radius};
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _, control_radius};
|
||||
|
||||
pub struct Tooltip {
|
||||
text: SharedString,
|
||||
@@ -19,7 +19,7 @@ impl Tooltip {
|
||||
impl Render for Tooltip {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
BaseTooltip::new("tooltip")
|
||||
.font_family(".SystemUIFont")
|
||||
.font_family(cx.theme().font_sans.clone())
|
||||
.m_3()
|
||||
.p_1p5()
|
||||
.border_1()
|
||||
@@ -27,9 +27,8 @@ impl Render for Tooltip {
|
||||
.bg(cx.theme().surface_overlay)
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.rounded(px(control_radius()))
|
||||
.text_xs()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text)
|
||||
.line_height(relative(1.25))
|
||||
.child(self.text.clone())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user