`gpui_web` currently embeds IBM Plex Sans and Lilex into every browser application during platform construction. Applications such as Delta already provide their own fonts, so those binaries contain overlapping font bundles and register the same faces twice. This change starts the web platform with an empty font database, matching the ownership boundary used by native applications: the application selects and registers its fonts before opening a window. Delta already follows that sequence. GPUI's browser-capable examples now load the former eight-face bundle explicitly through shared example support, while the standalone `hello_web` example demonstrates registering a minimal application font directly. Testing performed: - `cargo check -p gpui --examples --target wasm32-unknown-unknown` - `cargo check -p gpui --examples` - `cargo check -p gpui_web --target wasm32-unknown-unknown` - `cargo check --manifest-path crates/gpui_web/examples/hello_web/Cargo.toml --target wasm32-unknown-unknown` - `./script/clippy -p gpui_web --target wasm32-unknown-unknown` - `cargo fmt -p gpui -p gpui_web --check` Release Notes: - N/A
374 lines
18 KiB
Rust
374 lines
18 KiB
Rust
#![cfg_attr(target_family = "wasm", no_main)]
|
|
|
|
#[path = "example_support/fonts.rs"]
|
|
mod example_support;
|
|
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Result;
|
|
use gpui::{
|
|
Animation, AnimationExt as _, AnimationPhase, App, AssetSource, Bounds, Context, MouseButton,
|
|
MouseDownEvent, MouseMoveEvent, Pixels, SharedString, SpringAnimation, SpringConfig,
|
|
Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage,
|
|
prelude::*, px, relative, rgba, size, svg,
|
|
};
|
|
use gpui_platform::application;
|
|
|
|
struct Assets {}
|
|
|
|
impl AssetSource for Assets {
|
|
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
|
std::fs::read(path)
|
|
.map(Into::into)
|
|
.map_err(Into::into)
|
|
.map(Some)
|
|
}
|
|
|
|
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
|
Ok(std::fs::read_dir(path)?
|
|
.filter_map(|entry| {
|
|
Some(SharedString::from(
|
|
entry.ok()?.path().to_string_lossy().into_owned(),
|
|
))
|
|
})
|
|
.collect::<Vec<_>>())
|
|
}
|
|
}
|
|
|
|
const ARROW_CIRCLE_SVG: &str = concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/examples/image/arrow_circle.svg"
|
|
);
|
|
|
|
struct AnimationExample {
|
|
spring_phase: u8,
|
|
spring_damping: f32,
|
|
spring_damping_drag: Option<(Pixels, f32)>,
|
|
}
|
|
|
|
impl Render for AnimationExample {
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
const MINIMUM_DAMPING: f32 = 2.0;
|
|
const MAXIMUM_DAMPING: f32 = 32.0;
|
|
const DAMPING_SLIDER_WIDTH: Pixels = px(224.0);
|
|
|
|
let spring_phase = self.spring_phase;
|
|
let spring_position = px(98.0 * f32::from(spring_phase));
|
|
let spring_damping = self.spring_damping;
|
|
let spring = SpringConfig::new(170.0, spring_damping, 1.0);
|
|
let (_, damping_ratio) = spring.canonical();
|
|
let damping_fraction =
|
|
(spring_damping - MINIMUM_DAMPING) / (MAXIMUM_DAMPING - MINIMUM_DAMPING);
|
|
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.size_full()
|
|
.bg(gpui::white())
|
|
.text_color(gpui::black())
|
|
.justify_around()
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.size_full()
|
|
.justify_around()
|
|
.child(
|
|
div()
|
|
.id("content")
|
|
.flex()
|
|
.flex_col()
|
|
.h(px(150.))
|
|
.overflow_y_scroll()
|
|
.w_full()
|
|
.flex_1()
|
|
.justify_center()
|
|
.items_center()
|
|
.text_xl()
|
|
.gap_4()
|
|
.child("Hello Animation")
|
|
.child(
|
|
div()
|
|
.id("spring-demo")
|
|
.flex()
|
|
.flex_col()
|
|
.gap_2()
|
|
.p_2()
|
|
.w(px(240.0))
|
|
.rounded_md()
|
|
.bg(rgba(0x00000010))
|
|
.cursor_pointer()
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
cx.listener(|this, _, _, cx| {
|
|
this.spring_phase = (this.spring_phase + 1) % 3;
|
|
cx.notify();
|
|
}),
|
|
)
|
|
.child(format!(
|
|
"Target phase {spring_phase}: click rapidly to redirect momentum"
|
|
))
|
|
.child(
|
|
div()
|
|
.id("spring-damping-controls")
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.text_sm()
|
|
.on_click(|_, _, cx| cx.stop_propagation())
|
|
.child(format!(
|
|
"Drag damping: {spring_damping:.1} (ζ {damping_ratio:.2})"
|
|
))
|
|
.child(
|
|
div()
|
|
.id("spring-damping")
|
|
.relative()
|
|
.h(px(20.0))
|
|
.w(DAMPING_SLIDER_WIDTH)
|
|
.cursor_pointer()
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
cx.listener(
|
|
|this,
|
|
event: &MouseDownEvent,
|
|
_,
|
|
cx| {
|
|
this.spring_damping_drag = Some((
|
|
event.position.x,
|
|
this.spring_damping,
|
|
));
|
|
cx.stop_propagation();
|
|
},
|
|
),
|
|
)
|
|
.on_mouse_move(cx.listener(
|
|
|this,
|
|
event: &MouseMoveEvent,
|
|
_,
|
|
cx| {
|
|
let Some((
|
|
start_position,
|
|
start_damping,
|
|
)) = this.spring_damping_drag
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let delta = (event.position.x
|
|
- start_position)
|
|
/ DAMPING_SLIDER_WIDTH;
|
|
this.spring_damping = (start_damping
|
|
+ delta
|
|
* (MAXIMUM_DAMPING
|
|
- MINIMUM_DAMPING))
|
|
.clamp(
|
|
MINIMUM_DAMPING,
|
|
MAXIMUM_DAMPING,
|
|
);
|
|
cx.stop_propagation();
|
|
cx.notify();
|
|
},
|
|
))
|
|
.on_mouse_up(
|
|
MouseButton::Left,
|
|
cx.listener(|this, _, _, cx| {
|
|
this.spring_damping_drag = None;
|
|
cx.stop_propagation();
|
|
}),
|
|
)
|
|
.on_mouse_up_out(
|
|
MouseButton::Left,
|
|
cx.listener(|this, _, _, cx| {
|
|
this.spring_damping_drag = None;
|
|
cx.stop_propagation();
|
|
}),
|
|
)
|
|
.child(
|
|
div()
|
|
.absolute()
|
|
.top(px(8.0))
|
|
.h(px(4.0))
|
|
.w_full()
|
|
.rounded_full()
|
|
.bg(rgba(0x00000030)),
|
|
)
|
|
.child(
|
|
div()
|
|
.absolute()
|
|
.top(px(8.0))
|
|
.h(px(4.0))
|
|
.w(relative(damping_fraction))
|
|
.rounded_full()
|
|
.bg(rgba(0x3b82f6ff)),
|
|
)
|
|
.child(
|
|
div()
|
|
.absolute()
|
|
.top(px(3.0))
|
|
.left(relative(damping_fraction))
|
|
.ml(px(-7.0))
|
|
.size(px(14.0))
|
|
.rounded_full()
|
|
.bg(rgba(0x2563ebff)),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
div().relative().h(px(32.0)).w_full().child(
|
|
div()
|
|
.absolute()
|
|
.top_1()
|
|
.size(px(24.0))
|
|
.rounded_full()
|
|
.bg(rgba(0x3b82f6ff))
|
|
.cursor_pointer()
|
|
.debug_selector(|| "spring-position".into())
|
|
.with_spring(
|
|
"spring-position",
|
|
SpringAnimation::new(spring)
|
|
.to(spring_position)
|
|
.with_epsilon(0.25),
|
|
|this, left| this.left(left),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
div().h(px(24.0)).rounded_sm().with_spring(
|
|
"spring-phase",
|
|
SpringAnimation::new(spring)
|
|
.to(AnimationPhase(f32::from(spring_phase)))
|
|
.with_epsilon(0.001),
|
|
|this, phase| {
|
|
let (width, color) = if phase.0 <= 1.0 {
|
|
(
|
|
phase.interpolate_between(
|
|
0.0..=1.0,
|
|
px(48.0),
|
|
px(224.0),
|
|
),
|
|
phase.interpolate_between_clamped(
|
|
0.0..=1.0,
|
|
rgba(0xf97316ff),
|
|
rgba(0x22c55eff),
|
|
),
|
|
)
|
|
} else {
|
|
(
|
|
phase.interpolate_between(
|
|
1.0..=2.0,
|
|
px(224.0),
|
|
px(96.0),
|
|
),
|
|
phase.interpolate_between_clamped(
|
|
1.0..=2.0,
|
|
rgba(0x22c55eff),
|
|
rgba(0xa855f7ff),
|
|
),
|
|
)
|
|
};
|
|
this.w(width).bg(color)
|
|
},
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
svg()
|
|
.size_20()
|
|
.overflow_hidden()
|
|
.path(ARROW_CIRCLE_SVG)
|
|
.text_color(gpui::black())
|
|
.with_animation(
|
|
"image_circle",
|
|
Animation::new(Duration::from_secs(2))
|
|
.repeat()
|
|
.with_easing(bounce(ease_in_out)),
|
|
|svg, delta| {
|
|
svg.with_transformation(Transformation::rotate(
|
|
percentage(delta),
|
|
))
|
|
},
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.h(px(64.))
|
|
.w_full()
|
|
.p_2()
|
|
.justify_center()
|
|
.items_center()
|
|
.border_t_1()
|
|
.border_color(gpui::black().opacity(0.1))
|
|
.bg(gpui::black().opacity(0.05))
|
|
.child("Other Panel"),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn run_example() {
|
|
application().with_assets(Assets {}).run(|cx: &mut App| {
|
|
if !example_support::load_fonts(cx) {
|
|
return;
|
|
}
|
|
let options = WindowOptions {
|
|
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
|
None,
|
|
size(px(300.), px(300.)),
|
|
cx,
|
|
))),
|
|
..Default::default()
|
|
};
|
|
cx.open_window(options, |_, cx| {
|
|
cx.activate(false);
|
|
cx.new(|_| AnimationExample {
|
|
spring_phase: 0,
|
|
spring_damping: 14.0,
|
|
spring_damping_drag: None,
|
|
})
|
|
})
|
|
.unwrap();
|
|
});
|
|
}
|
|
|
|
#[cfg(not(target_family = "wasm"))]
|
|
fn main() {
|
|
run_example();
|
|
}
|
|
|
|
#[cfg(target_family = "wasm")]
|
|
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
|
pub fn start() {
|
|
gpui_platform::web_init();
|
|
run_example();
|
|
}
|
|
|
|
#[cfg(all(test, feature = "test-support"))]
|
|
mod tests {
|
|
use gpui::{Modifiers, TestAppContext};
|
|
|
|
use super::*;
|
|
|
|
#[gpui::test]
|
|
fn clicking_spring_position_changes_the_target(cx: &mut TestAppContext) {
|
|
let (view, cx) = cx.add_window_view(|_, _| AnimationExample {
|
|
spring_phase: 0,
|
|
spring_damping: 14.0,
|
|
spring_damping_drag: None,
|
|
});
|
|
cx.simulate_resize(size(px(300.0), px(300.0)));
|
|
cx.run_until_parked();
|
|
|
|
let position_bounds = cx
|
|
.debug_bounds("spring-position")
|
|
.expect("spring position should be rendered");
|
|
let position = position_bounds.center();
|
|
cx.simulate_mouse_move(position, None, Modifiers::default());
|
|
cx.simulate_click(position, Modifiers::default());
|
|
cx.run_until_parked();
|
|
|
|
assert_eq!(cx.update(|_, cx| view.read(cx).spring_phase), 1);
|
|
}
|
|
}
|