T01 primitives: image, divider, backdrop, focus, pressable, measure, tooltip
Ely-built primitives plus gallery demos for every chapter-one entry, including gpui natives. The gallery now scripts real input by posting NSEvents to its own queue and shoots each interaction in both modes. Codex gpt-6-astra (max): three rounds; the last finding, a two-line doc comment, was fixed after the three-review cap.
This commit is contained in:
38 files changed
+2165
-157
No files matched your search
@@ -0,0 +1,105 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, Div, ElementId, ImageSource, InteractiveElement, IntoElement,
|
||||
ObjectFit, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img,
|
||||
pulsating_between,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, IconSize},
|
||||
};
|
||||
|
||||
const PULSE: Duration = Duration::from_millis(1600);
|
||||
|
||||
/// Themed `img()`; a `Cover` crop keeps square corners.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Image {
|
||||
id: ElementId,
|
||||
base: Div,
|
||||
source: Option<ImageSource>,
|
||||
fit: ObjectFit,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// The id keeps gpui's loading state across frames.
|
||||
pub fn new(id: impl Into<ElementId>, source: impl Into<ImageSource>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
base: div(),
|
||||
source: Some(source.into()),
|
||||
fit: ObjectFit::Cover,
|
||||
}
|
||||
}
|
||||
|
||||
/// Nothing to load yet.
|
||||
pub fn placeholder(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
base: div(),
|
||||
source: None,
|
||||
fit: ObjectFit::Cover,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fit(mut self, fit: ObjectFit) -> Self {
|
||||
self.fit = fit;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Image {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Image {
|
||||
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let (sunken, subtle) = (theme.colors.sunken, theme.colors.fg_subtle);
|
||||
let still = theme.reduced_motion;
|
||||
let radii = self.base.style().corner_radii.clone();
|
||||
let pane = {
|
||||
let radii = radii.clone();
|
||||
move || {
|
||||
let mut pane = div().size_full().bg(sunken);
|
||||
pane.style().corner_radii = radii.clone();
|
||||
pane
|
||||
}
|
||||
};
|
||||
let loading_pane = pane.clone();
|
||||
let mark = move |name| {
|
||||
pane()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(Icon::new(name).size(IconSize::Lg).color(subtle))
|
||||
};
|
||||
let Some(source) = self.source else {
|
||||
return self.base.overflow_hidden().child(mark(IconName::Image));
|
||||
};
|
||||
let mut picture = img(source)
|
||||
.id(self.id)
|
||||
.size_full()
|
||||
.object_fit(self.fit)
|
||||
.with_loading(move || {
|
||||
if still {
|
||||
return loading_pane().into_any_element();
|
||||
}
|
||||
loading_pane()
|
||||
.with_animation(
|
||||
"image-loading",
|
||||
Animation::new(PULSE)
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.55, 1.0)),
|
||||
|pane, t| pane.opacity(t),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.with_fallback(move || mark(IconName::ImageOff).into_any_element());
|
||||
picture.style().corner_radii = radii;
|
||||
self.base.overflow_hidden().child(picture)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user