Vendor gpui-pre 0.3.8 unmodified (crates.io source)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-11 00:25:10 -04:00
1 parent ba5c0798cb
commit 48a66ca6c1
162 files changed
+104826 -2

No files matched your search

+398
View File
@@ -0,0 +1,398 @@
use smallvec::SmallVec;
use crate::{
Anchor, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId,
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
Window, point, px,
};
/// The state that the anchored element element uses to track its children.
pub struct AnchoredState {
child_layout_ids: SmallVec<[LayoutId; 4]>,
}
/// An anchored element that can be used to display UI that
/// will avoid overflowing the window bounds.
pub struct Anchored {
children: SmallVec<[AnyElement; 2]>,
anchor: Anchor,
fit_mode: AnchoredFitMode,
anchor_position: Option<Point<Pixels>>,
position_mode: AnchoredPositionMode,
offset: Option<Point<Pixels>>,
}
/// anchored gives you an element that will avoid overflowing the window bounds.
/// Its children should have no margin to avoid measurement issues.
pub fn anchored() -> Anchored {
Anchored {
children: SmallVec::new(),
anchor: Anchor::TopLeft,
fit_mode: AnchoredFitMode::SwitchAnchor,
anchor_position: None,
position_mode: AnchoredPositionMode::Window,
offset: None,
}
}
impl Anchored {
/// Sets which corner of the anchored element should be anchored to the current position.
pub fn anchor(mut self, anchor: Anchor) -> Self {
self.anchor = anchor;
self
}
/// Sets the position in window coordinates
/// (otherwise the location the anchored element is rendered is used)
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
self.anchor_position = Some(anchor);
self
}
/// Offset the final position by this amount.
/// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu.
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
self.offset = Some(offset);
self
}
/// Sets the position mode for this anchored element. Local will have this
/// interpret its [`Anchored::position`] as relative to the parent element.
/// While Window will have it interpret the position as relative to the window.
pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self {
self.position_mode = mode;
self
}
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
pub fn snap_to_window(mut self) -> Self {
self.fit_mode = AnchoredFitMode::SnapToWindow;
self
}
/// Snap to window edge and leave some margins.
pub fn snap_to_window_with_margin(mut self, edges: impl Into<Edges<Pixels>>) -> Self {
self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into());
self
}
}
impl ParentElement for Anchored {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl Element for Anchored {
type RequestLayoutState = AnchoredState;
type PrepaintState = ();
fn id(&self) -> Option<crate::ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
let child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(window, cx))
.collect::<SmallVec<_>>();
let anchored_style = Style {
position: Position::Absolute,
display: Display::Flex,
..Style::default()
};
let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx);
(layout_id, AnchoredState { child_layout_ids })
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) {
if request_layout.child_layout_ids.is_empty() {
return;
}
let children_bounds = request_layout
.child_layout_ids
.iter()
.map(|id| window.layout_bounds(*id))
.reduce(|acc, bounds| acc.union(&bounds))
.unwrap();
let (origin, mut desired) = self.position_mode.get_position_and_bounds(
self.anchor_position,
self.anchor,
children_bounds.size,
bounds,
self.offset,
);
let limits = Bounds {
origin: Point::default(),
size: window.viewport_size(),
};
if self.fit_mode == AnchoredFitMode::SwitchAnchor {
let mut anchor = self.anchor;
if desired.left() < limits.left() || desired.right() > limits.right() {
let switched = Bounds::from_anchor_and_size(
anchor.other_side_along(Axis::Horizontal),
origin,
children_bounds.size,
);
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
anchor = anchor.other_side_along(Axis::Horizontal);
desired = switched
}
}
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
let switched = Bounds::from_anchor_and_size(
anchor.other_side_along(Axis::Vertical),
origin,
children_bounds.size,
);
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
desired = switched;
}
}
}
let client_inset = window.client_inset.unwrap_or(px(0.));
let edges = match self.fit_mode {
AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
_ => Edges::default(),
}
.map(|edge| *edge + client_inset);
// Snap the horizontal edges of the anchored element to the horizontal edges of the window if
// its horizontal bounds overflow, aligning to the left if it is wider than the limits.
if desired.right() > limits.right() {
desired.origin.x -= desired.right() - limits.right() + edges.right;
}
if desired.left() < limits.left() {
desired.origin.x = limits.origin.x + edges.left;
}
// Snap the vertical edges of the anchored element to the vertical edges of the window if
// its vertical bounds overflow, aligning to the top if it is taller than the limits.
if desired.bottom() > limits.bottom() {
desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
}
if desired.top() < limits.top() {
desired.origin.y = limits.origin.y + edges.top;
}
let offset = desired.origin - bounds.origin;
let offset = point(offset.x.round(), offset.y.round());
window.with_element_offset(offset, |window| {
for child in &mut self.children {
child.prepaint(window, cx);
}
})
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
for child in &mut self.children {
child.paint(window, cx);
}
}
}
impl IntoElement for Anchored {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// Which algorithm to use when fitting the anchored element to be inside the window.
#[derive(Copy, Clone, PartialEq)]
pub enum AnchoredFitMode {
/// Snap the anchored element to the window edge.
SnapToWindow,
/// Snap to window edge and leave some margins.
SnapToWindowWithMargin(Edges<Pixels>),
/// Switch which corner anchor this anchored element is attached to.
SwitchAnchor,
}
/// Which algorithm to use when positioning the anchored element.
#[derive(Copy, Clone, PartialEq)]
pub enum AnchoredPositionMode {
/// Position the anchored element relative to the window.
Window,
/// Position the anchored element relative to its parent.
Local,
}
impl AnchoredPositionMode {
fn get_position_and_bounds(
&self,
anchor_position: Option<Point<Pixels>>,
anchor: Anchor,
size: Size<Pixels>,
bounds: Bounds<Pixels>,
offset: Option<Point<Pixels>>,
) -> (Point<Pixels>, Bounds<Pixels>) {
let offset = offset.unwrap_or_default();
match self {
AnchoredPositionMode::Window => {
let anchor_position = anchor_position.unwrap_or(bounds.origin);
let bounds = Bounds::from_anchor_and_size(anchor, anchor_position + offset, size);
(anchor_position, bounds)
}
AnchoredPositionMode::Local => {
let anchor_position = anchor_position.unwrap_or_default();
let bounds = Bounds::from_anchor_and_size(
anchor,
bounds.origin + anchor_position + offset,
size,
);
(anchor_position, bounds)
}
}
}
}
#[cfg(test)]
mod tests {
use crate::{
Context, Pixels, PlatformInput, Point, TestAppContext, Window, deferred, div, point,
prelude::*, px, size,
};
struct AnchoredTestView {
position: Point<Pixels>,
}
impl Render for AnchoredTestView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(
div()
.id("scroll-container")
.overflow_y_scroll()
.size_full()
.child(div().h(px(2000.)).w_full())
.child(
deferred(
super::anchored()
.snap_to_window()
.position(self.position)
.child(
div()
.id("menu")
.debug_selector(|| "MENU".into())
.w(px(200.))
.h(px(300.)),
),
)
.with_priority(1),
),
)
}
}
#[gpui::test]
fn test_anchored_position_without_scroll(cx: &mut TestAppContext) {
let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
position: point(px(100.), px(100.)),
});
cx.run_until_parked();
let menu_bounds = window
.update(cx, |_, window, _| {
window.rendered_frame.debug_bounds.get("MENU").copied()
})
.unwrap()
.expect("MENU debug bounds not found");
assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
}
#[gpui::test]
fn test_anchored_position_when_scrolled(cx: &mut TestAppContext) {
let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
position: point(px(100.), px(100.)),
});
cx.run_until_parked();
window
.update(cx, |_, window, cx| {
let event = gpui::ScrollWheelEvent {
position: point(px(400.), px(300.)),
delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-1000.))),
..Default::default()
};
window.dispatch_event(PlatformInput::ScrollWheel(event), cx);
})
.unwrap();
cx.run_until_parked();
let menu_bounds = window
.update(cx, |_, window, _| {
window.rendered_frame.debug_bounds.get("MENU").copied()
})
.unwrap()
.expect("MENU debug bounds not found");
assert_eq!(menu_bounds.origin, point(px(100.), px(100.)));
assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
}
#[gpui::test]
fn test_anchored_snaps_to_window(cx: &mut TestAppContext) {
let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView {
position: point(px(100.), px(500.)),
});
cx.run_until_parked();
let menu_bounds = window
.update(cx, |_, window, _| {
window.rendered_frame.debug_bounds.get("MENU").copied()
})
.unwrap()
.expect("MENU debug bounds not found");
assert_eq!(menu_bounds.origin, point(px(100.), px(300.)));
assert_eq!(menu_bounds.size, size(px(200.), px(300.)));
}
}
File diff suppressed because it is too large. Load diff
+95
View File
@@ -0,0 +1,95 @@
use refineable::Refineable as _;
use crate::{
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Pixels,
Style, StyleRefinement, Styled, Window,
};
/// Construct a canvas element with the given paint callback.
/// Useful for adding short term custom drawing to a view.
pub fn canvas<T>(
prepaint: impl 'static + FnOnce(Bounds<Pixels>, &mut Window, &mut App) -> T,
paint: impl 'static + FnOnce(Bounds<Pixels>, T, &mut Window, &mut App),
) -> Canvas<T> {
Canvas {
prepaint: Some(Box::new(prepaint)),
paint: Some(Box::new(paint)),
style: StyleRefinement::default(),
}
}
/// A canvas element, meant for accessing the low level paint API without defining a whole
/// custom element
pub struct Canvas<T> {
prepaint: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut Window, &mut App) -> T>>,
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, T, &mut Window, &mut App)>>,
style: StyleRefinement,
}
impl<T: 'static> IntoElement for Canvas<T> {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl<T: 'static> Element for Canvas<T> {
type RequestLayoutState = Style;
type PrepaintState = Option<T>;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (crate::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = window.request_layout(style.clone(), [], cx);
(layout_id, style)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Style,
window: &mut Window,
cx: &mut App,
) -> Option<T> {
Some(self.prepaint.take().unwrap()(bounds, window, cx))
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
style: &mut Style,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let prepaint = prepaint.take().unwrap();
style.paint(bounds, window, cx, |window, cx| {
(self.paint.take().unwrap())(bounds, prepaint, window, cx)
});
}
}
impl<T> Styled for Canvas<T> {
fn style(&mut self) -> &mut crate::StyleRefinement {
&mut self.style
}
}
+126
View File
@@ -0,0 +1,126 @@
//! A container query element, in the spirit of CSS container queries.
//! The element's own size is determined solely by its style and the space
//! offered by its parent.
use refineable::Refineable as _;
use crate::{
AnyElement, App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId,
InspectorElementId, IntoElement, LayoutId, Pixels, Size, Style, StyleRefinement, Styled,
Window, relative,
};
/// Construct a container query element with the given render callback.
/// The callback receives the size the element was assigned during layout and
/// returns the contents to display within it.
///
/// By default the element fills its parent (equivalent to `.size_full()`);
/// use the [`Styled`] methods to size it differently. Because the contents
/// don't exist until after layout, they cannot influence the element's size.
///
/// # Example
///
/// ```
/// # use gpui::{container_query, div, px, IntoElement, ParentElement};
/// container_query(|size, _window, _cx| {
/// if size.width < px(240.) {
/// div().child("Narrow layout")
/// } else {
/// div().child("Wide layout")
/// }
/// });
/// ```
pub fn container_query<E>(
render: impl 'static + FnOnce(Size<Pixels>, &mut Window, &mut App) -> E,
) -> ContainerQuery
where
E: IntoElement,
{
let mut base_style = StyleRefinement::default();
base_style.size.width = Some(relative(1.).into());
base_style.size.height = Some(relative(1.).into());
ContainerQuery {
render: Some(Box::new(|size, window, cx| {
render(size, window, cx).into_any_element()
})),
style: base_style,
}
}
/// A container query element, created with [`container_query`].
pub struct ContainerQuery {
render: Option<Box<dyn FnOnce(Size<Pixels>, &mut Window, &mut App) -> AnyElement>>,
style: StyleRefinement,
}
impl Element for ContainerQuery {
type RequestLayoutState = ();
type PrepaintState = Option<AnyElement>;
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = window.request_layout(style, [], cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
let render = self.render.take()?;
let mut child = render(bounds.size, window, cx);
child.layout_as_root(bounds.size.map(AvailableSpace::Definite), window, cx);
child.prepaint_at(bounds.origin, window, cx);
Some(child)
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
if let Some(child) = prepaint {
child.paint(window, cx);
}
}
}
impl IntoElement for ContainerQuery {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for ContainerQuery {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
+206
View File
@@ -0,0 +1,206 @@
use crate::{
AnyElement, App, Bounds, Element, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
Pixels, Window,
};
/// Builds a `Deferred` element, which delays the layout and paint of its child.
pub fn deferred(child: impl IntoElement) -> Deferred {
Deferred {
child: Some(child.into_any_element()),
priority: 0,
}
}
/// An element which delays the painting of its child until after all of
/// its ancestors, while keeping its layout as part of the current element tree.
pub struct Deferred {
child: Option<AnyElement>,
priority: usize,
}
impl Deferred {
/// Sets the `priority` value of the `deferred` element, which
/// determines the drawing order relative to other deferred elements,
/// with higher values being drawn on top.
pub fn with_priority(mut self, priority: usize) -> Self {
self.priority = priority;
self
}
}
impl Element for Deferred {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<crate::ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, ()) {
let layout_id = self.child.as_mut().unwrap().request_layout(window, cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
_cx: &mut App,
) {
let child = self.child.take().unwrap();
let element_offset = window.element_offset();
window.defer_draw(child, element_offset, self.priority, None)
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
_window: &mut Window,
_cx: &mut App,
) {
}
}
impl IntoElement for Deferred {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Deferred {
/// Sets a priority for the element. A higher priority conceptually means painting the element
/// on top of deferred draws with a lower priority (i.e. closer to the viewer).
pub fn priority(mut self, priority: usize) -> Self {
self.priority = priority;
self
}
}
#[cfg(test)]
mod tests {
use crate::{
Context, Entity, StyleRefinement, TestAppContext, Window, anchored, deferred, div, point,
prelude::*, px, size,
};
/// A stand-in for a dock panel hosting a popover (deferred draw) whose
/// content opens another popover (a deferred draw created while
/// prepainting the first one's content).
struct PanelView;
impl Render for PanelView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().key_context("Panel").size_full().child(
deferred(
anchored().position(point(px(10.), px(10.))).child(
div().key_context("Popover").w(px(200.)).h(px(200.)).child(
deferred(
anchored().position(point(px(30.), px(30.))).child(
div()
.key_context("NestedMenu")
.debug_selector(|| "NESTED_MENU".into())
.w(px(50.))
.h(px(50.)),
),
)
.with_priority(2),
),
),
)
.with_priority(1),
)
}
}
struct RootView {
panel: Entity<PanelView>,
}
impl Render for RootView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().key_context("Root").size_full().child(
self.panel
.clone()
.cached(StyleRefinement::default().size_full()),
)
}
}
/// Regression test for a crash with nested deferred draws (e.g. a popover
/// menu inside a popover hosted by a cached dock panel). Prepaint indices
/// recorded during the deferred draw rounds must index the same
/// `deferred_draws` vector that `reuse_prepaint` slices on the next frame;
/// previously they were measured against a transient per-round vector, so
/// reusing the panel's subtree grafted the wrong deferred draws and
/// panicked in the dispatch tree.
#[gpui::test]
fn test_nested_deferred_draws_with_reused_views(cx: &mut TestAppContext) {
let window = cx.open_window(size(px(800.), px(600.)), |_, cx| {
let panel = cx.new(|_| PanelView);
RootView { panel }
});
cx.run_until_parked();
let menu_bounds = window
.update(cx, |_, window, _| {
window
.rendered_frame
.debug_bounds
.get("NESTED_MENU")
.copied()
})
.unwrap()
.expect("NESTED_MENU debug bounds not found");
assert_eq!(menu_bounds.size, size(px(50.), px(50.)));
// Re-render only the root view; the panel is cached, so its subtree -
// including both deferred draw records - is reused from the previous
// frame.
window.update(cx, |_, _, cx| cx.notify()).unwrap();
cx.run_until_parked();
// Reuse the subtree a second time, exercising ranges that were
// themselves recorded during a reused frame.
window.update(cx, |_, _, cx| cx.notify()).unwrap();
cx.run_until_parked();
// Re-render the panel itself again to prove the popovers still draw.
window
.update(cx, |root, _, cx| {
root.panel.update(cx, |_, cx| cx.notify());
})
.unwrap();
cx.run_until_parked();
window
.update(cx, |_, window, _| {
assert_eq!(window.rendered_frame.deferred_draws.len(), 2);
assert!(
window
.rendered_frame
.debug_bounds
.contains_key("NESTED_MENU")
);
})
.unwrap();
}
}
File diff suppressed because it is too large. Load diff
+330
View File
@@ -0,0 +1,330 @@
use crate::{
AnyElement, AnyEntity, App, AppContext, Asset, AssetLogger, Bounds, Element, ElementId, Entity,
GlobalElementId, ImageAssetLoader, ImageCacheError, InspectorElementId, IntoElement, LayoutId,
ParentElement, Pixels, RenderImage, Resource, Style, StyleRefinement, Styled, Window, hash,
};
use crate::asset_cache::CachedLoad;
use refineable::Refineable;
use smallvec::SmallVec;
use std::{collections::HashMap, fmt, sync::Arc};
/// An image cache element, all its child img elements will use the cache specified by this element.
/// Note that this could as simple as passing an `Entity<T: ImageCache>`
pub fn image_cache(image_cache_provider: impl ImageCacheProvider) -> ImageCacheElement {
ImageCacheElement {
image_cache_provider: Box::new(image_cache_provider),
style: StyleRefinement::default(),
children: SmallVec::default(),
}
}
/// A dynamically typed image cache, which can be used to store any image cache
#[derive(Clone)]
pub struct AnyImageCache {
image_cache: AnyEntity,
load_fn: fn(
image_cache: &AnyEntity,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>>,
}
impl<I: ImageCache> From<Entity<I>> for AnyImageCache {
fn from(image_cache: Entity<I>) -> Self {
Self {
image_cache: image_cache.into_any(),
load_fn: any_image_cache::load::<I>,
}
}
}
impl AnyImageCache {
/// Load an image given a resource
/// returns the result of loading the image if it has finished loading, or None if it is still loading
pub fn load(
&self,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
(self.load_fn)(&self.image_cache, resource, window, cx)
}
}
mod any_image_cache {
use super::*;
pub(crate) fn load<I: 'static + ImageCache>(
image_cache: &AnyEntity,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
let image_cache = image_cache.clone().downcast::<I>().unwrap();
image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx))
}
}
/// An image cache element.
pub struct ImageCacheElement {
image_cache_provider: Box<dyn ImageCacheProvider>,
style: StyleRefinement,
children: SmallVec<[AnyElement; 2]>,
}
impl ParentElement for ImageCacheElement {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl Styled for ImageCacheElement {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl IntoElement for ImageCacheElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for ImageCacheElement {
type RequestLayoutState = SmallVec<[LayoutId; 4]>;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let image_cache = self.image_cache_provider.provide(window, cx);
window.with_image_cache(Some(image_cache), |window| {
let child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(window, cx))
.collect::<SmallVec<_>>();
let mut style = Style::default();
style.refine(&self.style);
let layout_id = window.request_layout(style, child_layout_ids.iter().copied(), cx);
(layout_id, child_layout_ids)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
for child in &mut self.children {
child.prepaint(window, cx);
}
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let image_cache = self.image_cache_provider.provide(window, cx);
window.with_image_cache(Some(image_cache), |window| {
for child in &mut self.children {
child.paint(window, cx);
}
})
}
}
/// Owns an image load and its cached result.
///
/// Dropping the entry cancels a pending load. Image caches must remove loaded
/// images from windows before discarding the entry.
pub struct ImageCacheItem(CachedLoad<Result<Arc<RenderImage>, ImageCacheError>>);
impl std::fmt::Debug for ImageCacheItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageCacheItem")
.field("result", &self.get())
.finish()
}
}
impl ImageCacheItem {
/// Starts loading an image into a new cache entry.
pub fn new(source: &Resource, cx: &mut App) -> Self {
let future = AssetLogger::<ImageAssetLoader>::load(source.clone(), cx);
Self(CachedLoad::new(future, cx))
}
/// Returns the cached result without subscribing to completion notifications.
pub fn get(&self) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
self.0.get()
}
/// Returns the cached result or subscribes the current view to completion.
pub fn use_image(&self, window: &Window) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
self.0.use_by(window.current_view())
}
}
/// An object that can handle the caching and unloading of images.
/// Implementations of this trait should ensure that images are removed from all windows when they are no longer needed.
pub trait ImageCache: 'static {
/// Load an image given a resource
/// returns the result of loading the image if it has finished loading, or None if it is still loading
fn load(
&mut self,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>>;
}
/// An object that can create an ImageCache during the render phase.
/// See the ImageCache trait for more information.
pub trait ImageCacheProvider: 'static {
/// Called during the request_layout phase to create an ImageCache.
fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache;
}
impl<T: ImageCache> ImageCacheProvider for Entity<T> {
fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache {
self.clone().into()
}
}
/// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full
pub struct RetainAllImageCache(HashMap<u64, ImageCacheItem>);
impl fmt::Debug for RetainAllImageCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HashMapImageCache")
.field("num_images", &self.0.len())
.finish()
}
}
impl RetainAllImageCache {
/// Create a new image cache.
#[inline]
pub fn new(cx: &mut App) -> Entity<Self> {
let e = cx.new(|_cx| RetainAllImageCache(HashMap::new()));
cx.observe_release(&e, |image_cache, cx| {
for (_, item) in std::mem::replace(&mut image_cache.0, HashMap::new()) {
if let Some(Ok(image)) = item.get() {
cx.drop_image(image, None);
}
}
})
.detach();
e
}
/// Load an image from the given source.
///
/// Returns `None` if the image is loading.
pub fn load(
&mut self,
source: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
let hash = hash(source);
self.0
.entry(hash)
.or_insert_with(|| ImageCacheItem::new(source, cx))
.use_image(window)
}
/// Clear the image cache.
pub fn clear(&mut self, window: &mut Window, cx: &mut App) {
for (_, item) in std::mem::replace(&mut self.0, HashMap::new()) {
if let Some(Ok(image)) = item.get() {
cx.drop_image(image, Some(window));
}
}
}
/// Remove the image from the cache by the given source.
pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) {
let hash = hash(source);
if let Some(item) = self.0.remove(&hash)
&& let Some(Ok(image)) = item.get()
{
cx.drop_image(image, Some(window));
}
}
/// Returns the number of images in the cache.
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns true if the cache is empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl ImageCache for RetainAllImageCache {
fn load(
&mut self,
resource: &Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
RetainAllImageCache::load(self, resource, window, cx)
}
}
/// Constructs a retain-all image cache that uses the element state associated with the given ID.
pub fn retain_all(id: impl Into<ElementId>) -> RetainAllImageCacheProvider {
RetainAllImageCacheProvider { id: id.into() }
}
/// A provider struct for creating a retain-all image cache inline
pub struct RetainAllImageCacheProvider {
id: ElementId,
}
impl ImageCacheProvider for RetainAllImageCacheProvider {
fn provide(&mut self, window: &mut Window, cx: &mut App) -> AnyImageCache {
window
.with_global_id(self.id.clone(), |global_id, window| {
window.with_element_state::<Entity<RetainAllImageCache>, _>(
global_id,
|cache, _window| {
let mut cache = cache.unwrap_or_else(|| RetainAllImageCache::new(cx));
(cache.clone(), cache)
},
)
})
.into()
}
}
+983
View File
@@ -0,0 +1,983 @@
use crate::{
AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId,
Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
SharedString, SharedUri, StyleRefinement, Styled, Task, Window, decode_static_image,
decode_static_image_from_decoder, px,
};
use anyhow::Result;
use futures::Future;
use gpui_util::ResultExt;
use image::{
AnimationDecoder, ImageError, ImageFormat, Rgba,
codecs::{gif::GifDecoder, webp::WebPDecoder},
};
use scheduler::Instant;
use smallvec::SmallVec;
use std::{
fs,
io::{self, Cursor},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
time::Duration,
};
use thiserror::Error;
use super::{Stateful, StatefulInteractiveElement};
/// The delay before showing the loading state.
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
/// A type alias to the resource loader that the `img()` element uses.
///
/// Note: that this is only for Resources, like URLs or file paths.
/// Custom loaders, or external images will not use this asset loader
pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
/// A source of image content.
#[derive(Clone)]
pub enum ImageSource {
/// The image content will be loaded from some resource location
Resource(Resource),
/// Cached image data
Render(Arc<RenderImage>),
/// Cached image data
Image(Arc<Image>),
/// A custom loading function to use
Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
}
fn is_uri(uri: &str) -> bool {
url::Url::from_str(uri).is_ok()
}
impl From<SharedUri> for ImageSource {
fn from(value: SharedUri) -> Self {
Self::Resource(Resource::Uri(value))
}
}
impl<'a> From<&'a str> for ImageSource {
fn from(s: &'a str) -> Self {
if is_uri(s) {
Self::Resource(Resource::Uri(s.to_string().into()))
} else {
Self::Resource(Resource::Embedded(s.to_string().into()))
}
}
}
impl From<String> for ImageSource {
fn from(s: String) -> Self {
if is_uri(&s) {
Self::Resource(Resource::Uri(s.into()))
} else {
Self::Resource(Resource::Embedded(s.into()))
}
}
}
impl From<SharedString> for ImageSource {
fn from(s: SharedString) -> Self {
s.as_ref().into()
}
}
impl From<&Path> for ImageSource {
fn from(value: &Path) -> Self {
Self::Resource(value.to_path_buf().into())
}
}
impl From<Arc<Path>> for ImageSource {
fn from(value: Arc<Path>) -> Self {
Self::Resource(value.into())
}
}
impl From<PathBuf> for ImageSource {
fn from(value: PathBuf) -> Self {
Self::Resource(value.into())
}
}
impl From<Arc<RenderImage>> for ImageSource {
fn from(value: Arc<RenderImage>) -> Self {
Self::Render(value)
}
}
impl From<Arc<Image>> for ImageSource {
fn from(value: Arc<Image>) -> Self {
Self::Image(value)
}
}
impl<F> From<F> for ImageSource
where
F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
{
fn from(value: F) -> Self {
Self::Custom(Arc::new(value))
}
}
/// The style of an image element.
pub struct ImageStyle {
grayscale: bool,
object_fit: ObjectFit,
loading: Option<Box<dyn Fn() -> AnyElement>>,
fallback: Option<Box<dyn Fn() -> AnyElement>>,
}
impl Default for ImageStyle {
fn default() -> Self {
Self {
grayscale: false,
object_fit: ObjectFit::Contain,
loading: None,
fallback: None,
}
}
}
/// Style an image element.
pub trait StyledImage: Sized {
/// Get a mutable [ImageStyle] from the element.
fn image_style(&mut self) -> &mut ImageStyle;
/// Set the image to be displayed in grayscale.
fn grayscale(mut self, grayscale: bool) -> Self {
self.image_style().grayscale = grayscale;
self
}
/// Set the object fit for the image.
fn object_fit(mut self, object_fit: ObjectFit) -> Self {
self.image_style().object_fit = object_fit;
self
}
/// Set a fallback function that will be invoked to render an error view should
/// the image fail to load.
fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().fallback = Some(Box::new(fallback));
self
}
/// Set a fallback function that will be invoked to render a view while the image
/// is still being loaded.
fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().loading = Some(Box::new(loading));
self
}
}
impl StyledImage for Img {
fn image_style(&mut self) -> &mut ImageStyle {
&mut self.style
}
}
impl StyledImage for Stateful<Img> {
fn image_style(&mut self) -> &mut ImageStyle {
&mut self.element.style
}
}
/// An image element.
pub struct Img {
interactivity: Interactivity,
source: ImageSource,
style: ImageStyle,
image_cache: Option<AnyImageCache>,
}
/// Create a new image element.
#[track_caller]
pub fn img(source: impl Into<ImageSource>) -> Img {
Img {
interactivity: Interactivity::new(),
source: source.into(),
style: ImageStyle::default(),
image_cache: None,
}
}
impl Img {
/// A list of all format extensions currently supported by this img element
pub fn extensions() -> &'static [&'static str] {
// This is the list in [image::ImageFormat::from_extension] + `svg`
&[
"avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
]
}
/// Sets the image cache for the current node.
///
/// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
///
/// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used.
/// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
///
/// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
/// while ensuring a default behavior when no cache is explicitly specified.
#[inline]
pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
Self {
image_cache: Some(image_cache.clone().into()),
..self
}
}
}
impl Deref for Stateful<Img> {
type Target = Img;
fn deref(&self) -> &Self::Target {
&self.element
}
}
impl DerefMut for Stateful<Img> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.element
}
}
/// The image state between frames
struct ImgState {
frame_index: usize,
last_frame_time: Option<Instant>,
started_loading: Option<(Instant, Task<()>)>,
}
/// The image layout state between frames
pub struct ImgLayoutState {
frame_index: usize,
replacement: Option<AnyElement>,
}
impl Element for Img {
type RequestLayoutState = ImgLayoutState;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
self.interactivity.source_location()
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut layout_state = ImgLayoutState {
frame_index: 0,
replacement: None,
};
window.with_optional_element_state(global_id, |state, window| {
let mut state = state.map(|state| {
state.unwrap_or(ImgState {
frame_index: 0,
last_frame_time: None,
started_loading: None,
})
});
let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
let layout_id = self.interactivity.request_layout(
global_id,
inspector_id,
window,
cx,
|mut style, window, cx| {
let mut replacement_id = None;
match self.source.use_data(
self.image_cache
.clone()
.or_else(|| window.image_cache_stack.last().cloned()),
window,
cx,
) {
Some(Ok(data)) => {
let frame_count = data.frame_count();
let max_frame_index = frame_count.saturating_sub(1);
if let Some(state) = &mut state {
state.frame_index = state.frame_index.min(max_frame_index);
if frame_count > 1 && !cx.reduce_motion() {
if window.is_window_active() {
let current_time = Instant::now();
if let Some(last_frame_time) = state.last_frame_time {
let elapsed = current_time - last_frame_time;
let frame_duration =
Duration::from(data.delay(state.frame_index));
if elapsed >= frame_duration {
state.frame_index =
(state.frame_index + 1) % frame_count;
state.last_frame_time =
Some(current_time - (elapsed - frame_duration));
}
} else {
state.last_frame_time = Some(current_time);
}
} else {
state.last_frame_time = None;
}
} else {
state.last_frame_time = None;
}
state.started_loading = None;
frame_index = state.frame_index;
}
let image_size = data.render_size(frame_index);
if style.aspect_ratio.is_none() {
style.aspect_ratio = Some(image_size.width / image_size.height);
}
if let Length::Auto = style.size.width {
style.size.width = match style.size.height {
Length::Definite(DefiniteLength::Absolute(abs_length)) => {
let height_px = abs_length.to_pixels(window.rem_size());
Length::Definite(
px(image_size.width.0 * height_px.0
/ image_size.height.0)
.into(),
)
}
_ => Length::Definite(image_size.width.into()),
};
}
if let Length::Auto = style.size.height {
style.size.height = match style.size.width {
Length::Definite(DefiniteLength::Absolute(abs_length)) => {
let width_px = abs_length.to_pixels(window.rem_size());
Length::Definite(
px(image_size.height.0 * width_px.0
/ image_size.width.0)
.into(),
)
}
_ => Length::Definite(image_size.height.into()),
};
}
if global_id.is_some()
&& data.frame_count() > 1
&& window.is_window_active()
&& !cx.reduce_motion()
{
window.request_animation_frame();
}
}
Some(_err) => {
if let Some(fallback) = self.style.fallback.as_ref() {
let mut element = fallback();
replacement_id = Some(element.request_layout(window, cx));
layout_state.replacement = Some(element);
}
if let Some(state) = &mut state {
state.started_loading = None;
}
}
None => {
if let Some(state) = &mut state {
if let Some((started_loading, _)) = state.started_loading {
if started_loading.elapsed() > LOADING_DELAY
&& let Some(loading) = self.style.loading.as_ref()
{
let mut element = loading();
replacement_id = Some(element.request_layout(window, cx));
layout_state.replacement = Some(element);
}
} else {
let current_view = window.current_view();
let task = window.spawn(cx, async move |cx| {
cx.background_executor().timer(LOADING_DELAY).await;
cx.update(move |_, cx| {
cx.notify(current_view);
})
.ok();
});
state.started_loading = Some((Instant::now(), task));
}
}
}
}
window.request_layout(style, replacement_id, cx)
},
);
layout_state.frame_index = frame_index;
((layout_id, layout_state), state)
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
bounds.size,
window,
cx,
|_, _, hitbox, window, cx| {
if let Some(replacement) = &mut request_layout.replacement {
replacement.prepaint(window, cx);
}
hitbox
},
)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let source = self.source.clone();
self.interactivity.paint(
global_id,
inspector_id,
bounds,
hitbox.as_ref(),
window,
cx,
|style, window, cx| {
if let Some(Ok(data)) = source.use_data(
self.image_cache
.clone()
.or_else(|| window.image_cache_stack.last().cloned()),
window,
cx,
) {
if data.frame_count() == 0 {
return;
}
let new_bounds = self
.style
.object_fit
.get_bounds(bounds, data.size(layout_state.frame_index));
let corner_radii = style.corner_radii.to_pixels(window.rem_size());
window
.paint_image(
bounds,
new_bounds,
corner_radii,
data,
layout_state.frame_index,
self.style.grayscale,
)
.log_err();
} else if let Some(replacement) = &mut layout_state.replacement {
replacement.paint(window, cx);
}
},
)
}
}
impl Styled for Img {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl InteractiveElement for Img {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
impl IntoElement for Img {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl StatefulInteractiveElement for Img {}
impl ImageSource {
pub(crate) fn use_data(
&self,
cache: Option<AnyImageCache>,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
ImageSource::Resource(resource) => {
if let Some(cache) = cache {
cache.load(resource, window, cx)
} else {
window.use_asset::<ImgResourceLoader>(resource, cx)
}
}
ImageSource::Custom(loading_fn) => loading_fn(window, cx),
ImageSource::Render(data) => Some(Ok(data.to_owned())),
ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
}
}
pub(crate) fn get_data(
&self,
cache: Option<AnyImageCache>,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
ImageSource::Resource(resource) => {
if let Some(cache) = cache {
cache.load(resource, window, cx)
} else {
window.get_asset::<ImgResourceLoader>(resource, cx)
}
}
ImageSource::Custom(loading_fn) => loading_fn(window, cx),
ImageSource::Render(data) => Some(Ok(data.to_owned())),
ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
}
}
/// Remove this image source from the asset system
pub fn remove_asset(&self, cx: &mut App) {
match self {
ImageSource::Resource(resource) => {
cx.remove_asset::<ImgResourceLoader>(resource);
}
ImageSource::Custom(_) | ImageSource::Render(_) => {}
ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
}
}
/// Check whether this image source is present in the asset system (loading
/// or loaded), without fetching it.
#[cfg(any(test, feature = "test-support"))]
pub fn is_asset_cached(&self, cx: &App) -> bool {
match self {
ImageSource::Resource(resource) => cx.has_asset::<ImgResourceLoader>(resource),
ImageSource::Custom(_) | ImageSource::Render(_) => false,
ImageSource::Image(data) => cx.has_asset::<AssetLogger<ImageDecoder>>(data),
}
}
}
#[derive(Clone)]
enum ImageDecoder {}
impl Asset for ImageDecoder {
type Source = Arc<Image>;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
source: Self::Source,
cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
let renderer = cx.svg_renderer();
async move { source.to_image_data(renderer).map_err(Into::into) }
}
}
/// An image loader for the GPUI asset system
#[derive(Clone)]
pub enum ImageAssetLoader {}
impl Asset for ImageAssetLoader {
type Source = Resource;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
source: Self::Source,
cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
let client = cx.http_client();
// TODO: Can we make SVGs always rescale?
// let scale_factor = cx.scale_factor();
let svg_renderer = cx.svg_renderer();
let asset_source = cx.asset_source().clone();
async move {
let bytes = match source.clone() {
Resource::Path(uri) => fs::read(uri.as_ref())?,
Resource::Uri(uri) => {
use anyhow::Context as _;
use futures::AsyncReadExt as _;
let mut response = client
.get(uri.as_ref(), ().into(), true)
.await
.with_context(|| format!("loading image asset from {uri:?}"))?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if !response.status().is_success() {
let mut body = String::from_utf8_lossy(&body).into_owned();
let first_line = body.lines().next().unwrap_or("").trim_end();
body.truncate(first_line.len());
return Err(ImageCacheError::BadStatus {
uri,
status: response.status(),
body,
});
}
body
}
Resource::Embedded(path) => {
let data = asset_source.load(&path).ok().flatten();
if let Some(data) = data {
data.to_vec()
} else {
return Err(ImageCacheError::Asset(
format!("Embedded resource not found: {}", path).into(),
));
}
}
};
if let Ok(format) = image::guess_format(&bytes) {
let data = match format {
ImageFormat::Gif => {
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
let mut frames = SmallVec::new();
for frame in decoder.into_frames() {
match frame {
Ok(mut frame) => {
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
Err(err) => {
log::debug!(
"Skipping GIF frame in {source:?} due to decode error: {err}"
);
}
}
}
if frames.is_empty() {
return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
"GIF could not be decoded: all frames failed ({source:?})"
))));
}
frames
}
ImageFormat::WebP => {
let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
if decoder.has_animation() {
let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
let mut frames = SmallVec::new();
for frame in decoder.into_frames() {
match frame {
Ok(mut frame) => {
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
Err(err) => {
log::debug!(
"Skipping WebP frame in {source:?} due to decode error: {err}"
);
}
}
}
if frames.is_empty() {
return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
"WebP could not be decoded: all frames failed ({source:?})"
))));
}
frames
} else {
decode_static_image_from_decoder(decoder)?
}
}
_ => decode_static_image(&bytes, format)?,
};
Ok(Arc::new(RenderImage::new(data)))
} else {
svg_renderer
.render_single_frame(&bytes, 1.0)
.map_err(Into::into)
}
}
}
}
/// An error that can occur when interacting with the image cache.
#[derive(Debug, Error, Clone)]
pub enum ImageCacheError {
/// Some other kind of error occurred
#[error("error: {0}")]
Other(#[from] Arc<anyhow::Error>),
/// An error that occurred while reading the image from disk.
#[error("IO error: {0}")]
Io(Arc<std::io::Error>),
/// An error that occurred while processing an image.
#[error("unexpected http status for {uri}: {status}, body: {body}")]
BadStatus {
/// The URI of the image.
uri: SharedUri,
/// The HTTP status code.
status: http_client::StatusCode,
/// The HTTP response body.
body: String,
},
/// An error that occurred while processing an asset.
#[error("asset error: {0}")]
Asset(SharedString),
/// An error that occurred while processing an image.
#[error("image error: {0}")]
Image(Arc<ImageError>),
/// An error that occurred while processing an SVG.
#[error("svg error: {0}")]
Usvg(Arc<usvg::Error>),
}
impl From<anyhow::Error> for ImageCacheError {
fn from(value: anyhow::Error) -> Self {
Self::Other(Arc::new(value))
}
}
impl From<io::Error> for ImageCacheError {
fn from(value: io::Error) -> Self {
Self::Io(Arc::new(value))
}
}
impl From<usvg::Error> for ImageCacheError {
fn from(value: usvg::Error) -> Self {
Self::Usvg(Arc::new(value))
}
}
impl From<image::ImageError> for ImageCacheError {
fn from(value: image::ImageError) -> Self {
Self::Image(Arc::new(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ParentElement as _, TestAppContext, canvas, div, point, px, size};
use image::{Frame, ImageBuffer, Rgba};
const TEST_IMG_ID: &str = "test-img";
fn test_image(frame_count: usize) -> Arc<RenderImage> {
let frame = Frame::new(ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0])));
Arc::new(RenderImage::new(SmallVec::from_iter(
(0..frame_count).map(|_| frame.clone()),
)))
}
fn test_image_with_size(width: u32, height: u32) -> Arc<RenderImage> {
let frame = Frame::new(ImageBuffer::from_pixel(width, height, Rgba([0, 0, 0, 0])));
Arc::new(RenderImage::new(SmallVec::from_elem(frame, 1)))
}
/// Overwrites the cached `frame_index` of the sibling `img` during paint.
fn seed_frame_index(frame_index: usize) -> impl IntoElement {
canvas(
|_, _, _| (),
move |_, _, window, _| {
window.with_global_id(TEST_IMG_ID.into(), |id, window| {
window.with_element_state::<ImgState, _>(id, |state, _| {
let mut state = state.expect("img state should be initialized");
state.frame_index = frame_index;
((), state)
});
});
},
)
}
#[gpui::test]
fn zero_frame_image_does_not_panic_on_paint(cx: &mut TestAppContext) {
cx.add_empty_window()
.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
img(ImageSource::Render(test_image(0))).into_any_element()
});
}
#[gpui::test]
fn image_object_fit_cover_crops_to_element_bounds(cx: &mut TestAppContext) {
let window = cx.add_empty_window();
let image = test_image_with_size(200, 100);
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
img(ImageSource::Render(image.clone()))
.size_full()
.object_fit(ObjectFit::Fill)
.into_any_element()
});
let full_tile_bounds = window.update(|window, _| {
window
.rendered_frame
.scene
.polychrome_sprites
.last()
.expect("fill image should paint a sprite")
.tile
.bounds
});
window.draw(point(px(10.), px(20.)), size(px(100.), px(100.)), |_, _| {
img(ImageSource::Render(image))
.size_full()
.object_fit(ObjectFit::Cover)
.into_any_element()
});
let (rendered_bounds, rendered_tile_bounds, scale_factor) = window.update(|window, _| {
let sprite = window
.rendered_frame
.scene
.polychrome_sprites
.last()
.expect("cover image should paint a sprite");
(sprite.bounds, sprite.tile.bounds, window.scale_factor())
});
assert_eq!(
rendered_bounds,
Bounds {
origin: point(px(10.).scale(scale_factor), px(20.).scale(scale_factor)),
size: size(px(100.).scale(scale_factor), px(100.).scale(scale_factor)),
}
);
assert_eq!(
(
rendered_tile_bounds.origin.x.0 - full_tile_bounds.origin.x.0,
rendered_tile_bounds.origin.y.0 - full_tile_bounds.origin.y.0,
rendered_tile_bounds.size.width.0,
rendered_tile_bounds.size.height.0,
),
(50, 0, 100, 100),
);
}
#[gpui::test]
fn explicit_aspect_ratio_is_not_overridden_by_intrinsic_ratio(cx: &mut TestAppContext) {
let window = cx.add_empty_window();
// A portrait image in a square container
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
div()
.size(px(100.))
.overflow_hidden()
.child(
img(ImageSource::Render(test_image_with_size(100, 200)))
.size_full()
.aspect_square()
.object_fit(ObjectFit::Contain),
)
.into_any_element()
});
let (rendered_bounds, scale_factor) = window.update(|window, _| {
let sprite = window
.rendered_frame
.scene
.polychrome_sprites
.last()
.expect("contained image should paint a sprite");
(sprite.bounds, window.scale_factor())
});
// The element stays 100x100, so the image is letterboxed horizontally
assert_eq!(
rendered_bounds,
Bounds {
origin: point(px(25.).scale(scale_factor), px(0.).scale(scale_factor)),
size: size(px(50.).scale(scale_factor), px(100.).scale(scale_factor)),
}
);
}
#[gpui::test]
fn image_object_fit_cover_clamps_corner_radii_to_visible_bounds(cx: &mut TestAppContext) {
let window = cx.add_empty_window();
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
img(ImageSource::Render(test_image_with_size(200, 100)))
.size_full()
.rounded(px(100.))
.object_fit(ObjectFit::Cover)
.into_any_element()
});
let (corner_radius, expected_corner_radius) = window.update(|window, _| {
(
window
.rendered_frame
.scene
.polychrome_sprites
.last()
.map(|sprite| sprite.corner_radii.top_left),
px(50.).scale(window.scale_factor()),
)
});
assert_eq!(corner_radius, Some(expected_corner_radius));
}
#[gpui::test]
fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) {
let window = cx.add_empty_window();
// Assert that a cached frame_index from a previous multi-frame image
// does not cause an out-of-bounds panic when the image is replaced
// with one that has fewer frames.
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
div()
.child(img(ImageSource::Render(test_image(5))).id(TEST_IMG_ID))
.child(seed_frame_index(4))
.into_any_element()
});
window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
img(ImageSource::Render(test_image(1)))
.id(TEST_IMG_ID)
.into_any_element()
});
}
}
File diff suppressed because it is too large. Load diff
+27
View File
@@ -0,0 +1,27 @@
mod anchored;
mod animation;
mod canvas;
mod container_query;
mod deferred;
mod div;
mod image_cache;
mod img;
mod list;
mod surface;
mod svg;
mod text;
mod uniform_list;
pub use anchored::*;
pub use animation::*;
pub use canvas::*;
pub use container_query::*;
pub use deferred::*;
pub use div::*;
pub use image_cache::*;
pub use img::*;
pub use list::*;
pub use surface::*;
pub use svg::*;
pub use text::*;
pub use uniform_list::*;
+129
View File
@@ -0,0 +1,129 @@
use crate::{
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
ObjectFit, Pixels, Style, StyleRefinement, Styled, Window,
};
#[cfg(any(target_os = "macos", target_os = "ios"))]
use core_video::pixel_buffer::CVPixelBuffer;
use refineable::Refineable;
/// A source of a surface's content.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SurfaceSource {
/// A CoreVideo image buffer
#[cfg(any(target_os = "macos", target_os = "ios"))]
Surface(CVPixelBuffer),
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
impl From<CVPixelBuffer> for SurfaceSource {
fn from(value: CVPixelBuffer) -> Self {
SurfaceSource::Surface(value)
}
}
/// A surface element.
pub struct Surface {
source: SurfaceSource,
object_fit: ObjectFit,
style: StyleRefinement,
}
/// Create a new surface element.
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub fn surface(source: impl Into<SurfaceSource>) -> Surface {
Surface {
source: source.into(),
object_fit: ObjectFit::Contain,
style: Default::default(),
}
}
impl Surface {
/// Set the object fit for the image.
pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
self.object_fit = object_fit;
self
}
}
impl Element for Surface {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = window.request_layout(style, [], cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) -> Self::PrepaintState {
}
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
#[cfg_attr(
not(any(target_os = "macos", target_os = "ios")),
allow(unused_variables)
)]
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
#[cfg_attr(
not(any(target_os = "macos", target_os = "ios")),
allow(unused_variables)
)]
window: &mut Window,
_: &mut App,
) {
match &self.source {
#[cfg(any(target_os = "macos", target_os = "ios"))]
SurfaceSource::Surface(surface) => {
let size = crate::size(surface.get_width().into(), surface.get_height().into());
let new_bounds = self.object_fit.get_bounds(bounds, size);
// TODO: Add support for corner_radii
window.paint_surface(new_bounds, surface.clone());
}
#[allow(unreachable_patterns)]
_ => {}
}
}
}
impl IntoElement for Surface {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for Surface {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
+303
View File
@@ -0,0 +1,303 @@
use std::{
fs,
hash::{Hash, Hasher},
path::Path,
sync::Arc,
};
use crate::{
App, Asset, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
StyleRefinement, Styled, TransformationMatrix, Window, point, px, radians, size,
};
use gpui_util::ResultExt;
/// An SVG element.
pub struct Svg {
interactivity: Interactivity,
transformation: Option<Transformation>,
path: Option<SharedString>,
external_path: Option<SharedString>,
data: Option<Arc<[u8]>>,
data_path: Option<SharedString>,
}
/// Create a new SVG element.
#[track_caller]
pub fn svg() -> Svg {
Svg {
interactivity: Interactivity::new(),
transformation: None,
path: None,
external_path: None,
data: None,
data_path: None,
}
}
impl Svg {
/// Set the path to the SVG file for this element.
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = Some(path.into());
self
}
/// Set the path to the SVG file for this element.
pub fn external_path(mut self, path: impl Into<SharedString>) -> Self {
self.external_path = Some(path.into());
self
}
/// Set the raw SVG data for this element.
/// The SVG will be rendered directly from the provided bytes.
pub fn data(mut self, data: &[u8]) -> Self {
// Generate a unique deterministic path based on the data hash for caching
let mut hasher = std::collections::hash_map::DefaultHasher::new();
data.hash(&mut hasher);
let hash = hasher.finish();
let path = SharedString::from(format!("__binary_svg__{}", hash));
self.data = Some(Arc::from(data));
self.data_path = Some(path);
self
}
/// Transform the SVG element with the given transformation.
/// Note that this won't effect the hitbox or layout of the element, only the rendering.
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
self.transformation = Some(transformation);
self
}
}
impl Element for Svg {
type RequestLayoutState = ();
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<crate::ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
self.interactivity.source_location()
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let layout_id = self.interactivity.request_layout(
global_id,
inspector_id,
window,
cx,
|style, window, cx| window.request_layout(style, None, cx),
);
(layout_id, ())
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Option<Hitbox> {
self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
bounds.size,
window,
cx,
|_, _, hitbox, _, _| hitbox,
)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
hitbox: &mut Option<Hitbox>,
window: &mut Window,
cx: &mut App,
) where
Self: Sized,
{
self.interactivity.paint(
global_id,
inspector_id,
bounds,
hitbox.as_ref(),
window,
cx,
|style, window, cx| {
let transformation = self
.transformation
.as_ref()
.map(|transformation| {
transformation.into_matrix(bounds.center(), window.scale_factor())
})
.unwrap_or_default();
if let Some((data, path)) = self.data.as_ref().zip(self.data_path.as_ref()) {
if let Some(color) = style.text.color {
window
.paint_svg(
bounds,
path.clone(),
Some(&**data),
transformation,
color,
cx,
)
.log_err();
}
} else if let Some((path, color)) =
self.external_path.as_ref().zip(style.text.color)
{
let Some(bytes) = window
.use_asset::<SvgAsset>(path, cx)
.and_then(|asset| asset.log_err())
else {
return;
};
window
.paint_svg(
bounds,
path.clone(),
Some(&bytes),
transformation,
color,
cx,
)
.log_err();
} else if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
window
.paint_svg(bounds, path.clone(), None, transformation, color, cx)
.log_err();
}
},
)
}
}
impl IntoElement for Svg {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for Svg {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl InteractiveElement for Svg {
fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
/// A transformation to apply to an SVG element.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transformation {
scale: Size<f32>,
translate: Point<Pixels>,
rotate: Radians,
}
impl Default for Transformation {
fn default() -> Self {
Self {
scale: size(1.0, 1.0),
translate: point(px(0.0), px(0.0)),
rotate: radians(0.0),
}
}
}
impl Transformation {
/// Create a new Transformation with the specified scale along each axis.
pub fn scale(scale: Size<f32>) -> Self {
Self {
scale,
translate: point(px(0.0), px(0.0)),
rotate: radians(0.0),
}
}
/// Create a new Transformation with the specified translation.
pub fn translate(translate: Point<Pixels>) -> Self {
Self {
scale: size(1.0, 1.0),
translate,
rotate: radians(0.0),
}
}
/// Create a new Transformation with the specified rotation in radians.
pub fn rotate(rotate: impl Into<Radians>) -> Self {
let rotate = rotate.into();
Self {
scale: size(1.0, 1.0),
translate: point(px(0.0), px(0.0)),
rotate,
}
}
/// Update the scaling factor of this transformation.
pub fn with_scaling(mut self, scale: Size<f32>) -> Self {
self.scale = scale;
self
}
/// Update the translation value of this transformation.
pub fn with_translation(mut self, translate: Point<Pixels>) -> Self {
self.translate = translate;
self
}
/// Update the rotation angle of this transformation.
pub fn with_rotation(mut self, rotate: impl Into<Radians>) -> Self {
self.rotate = rotate.into();
self
}
fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
//Note: if you read this as a sequence of matrix multiplications, start from the bottom
TransformationMatrix::unit()
.translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
.rotate(self.rotate)
.scale(self.scale)
.translate(center.scale(-scale_factor))
}
}
enum SvgAsset {}
impl Asset for SvgAsset {
type Source = SharedString;
type Output = Result<Arc<[u8]>, Arc<std::io::Error>>;
fn load(
source: Self::Source,
_cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
async move {
let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?;
let bytes = Arc::from(bytes);
Ok(bytes)
}
}
}
File diff suppressed because it is too large. Load diff
+865
View File
@@ -0,0 +1,865 @@
//! A scrollable list of elements with uniform height, optimized for large lists.
//! Rather than use the full taffy layout system, uniform_list simply measures
//! the first element and then lays out all remaining elements in a line based on that
//! measurement. This is much faster than the full layout system, but only works for
//! elements with uniform height.
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity,
GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size,
StyleRefinement, Styled, Window, point, px, size,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
use super::ListHorizontalSizingBehavior;
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
/// uniform_list will only render the visible subset of items.
#[track_caller]
pub fn uniform_list<R>(
id: impl Into<ElementId>,
item_count: usize,
f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
) -> UniformList
where
R: IntoElement,
{
let id = id.into();
let mut base_style = StyleRefinement::default();
base_style.overflow.y = Some(Overflow::Scroll);
let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
f(range, window, cx)
.into_iter()
.map(|component| component.into_any_element())
.collect()
};
UniformList {
item_count,
item_to_measure_index: 0,
render_items: Box::new(render_range),
decorations: Vec::new(),
interactivity: Interactivity {
element_id: Some(id),
base_style: Box::new(base_style),
..Interactivity::new()
},
scroll_handle: None,
sizing_behavior: ListSizingBehavior::default(),
horizontal_sizing_behavior: ListHorizontalSizingBehavior::default(),
}
}
/// A list element for efficiently laying out and displaying a list of uniform-height elements.
pub struct UniformList {
item_count: usize,
item_to_measure_index: usize,
render_items: Box<
dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
>,
decorations: Vec<Box<dyn UniformListDecoration>>,
interactivity: Interactivity,
scroll_handle: Option<UniformListScrollHandle>,
sizing_behavior: ListSizingBehavior,
horizontal_sizing_behavior: ListHorizontalSizingBehavior,
}
/// Frame state used by the [UniformList].
pub struct UniformListFrameState {
items: SmallVec<[AnyElement; 32]>,
decorations: SmallVec<[AnyElement; 2]>,
}
/// A handle for controlling the scroll position of a uniform list.
/// This should be stored in your view and passed to the uniform_list on each frame.
#[derive(Clone, Debug, Default)]
pub struct UniformListScrollHandle(pub Rc<RefCell<UniformListScrollState>>);
/// Where to place the element scrolled to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScrollStrategy {
/// Place the element at the top of the list's viewport.
Top,
/// Attempt to place the element in the middle of the list's viewport.
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Center,
/// Attempt to place the element at the bottom of the list's viewport.
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Bottom,
/// If the element is not visible attempt to place it at:
/// - The top of the list's viewport if the target element is above currently visible elements.
/// - The bottom of the list's viewport if the target element is above currently visible elements.
Nearest,
}
#[derive(Clone, Copy, Debug)]
#[allow(missing_docs)]
pub struct DeferredScrollToItem {
/// The item index to scroll to
pub item_index: usize,
/// The scroll strategy to use
pub strategy: ScrollStrategy,
/// The offset in number of items
pub offset: usize,
pub scroll_strict: bool,
}
#[derive(Clone, Debug, Default)]
#[allow(missing_docs)]
pub struct UniformListScrollState {
pub base_handle: ScrollHandle,
pub deferred_scroll_to_item: Option<DeferredScrollToItem>,
/// Size of the item, captured during last layout.
pub last_item_size: Option<ItemSize>,
/// Whether the list was vertically flipped during last layout.
pub y_flipped: bool,
}
#[derive(Copy, Clone, Debug, Default)]
/// The size of the item and its contents.
pub struct ItemSize {
/// The size of the item.
pub item: Size<Pixels>,
/// The size of the item's contents, which may be larger than the item itself,
/// if the item was bounded by a parent element.
pub contents: Size<Pixels>,
}
impl UniformListScrollHandle {
/// Create a new scroll handle to bind to a uniform list.
pub fn new() -> Self {
Self(Rc::new(RefCell::new(UniformListScrollState {
base_handle: ScrollHandle::new(),
deferred_scroll_to_item: None,
last_item_size: None,
y_flipped: false,
})))
}
/// Scroll the list so that the given item index is visible.
///
/// This uses non-strict scrolling: if the item is already fully visible, no scrolling occurs.
/// If the item is out of view, it scrolls the minimum amount to bring it into view according
/// to the strategy.
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset: 0,
scroll_strict: false,
});
}
/// Scroll the list so that the given item index is at scroll strategy position.
///
/// This uses strict scrolling: the item will always be scrolled to match the strategy position,
/// even if it's already visible. Use this when you need precise positioning.
pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset: 0,
scroll_strict: true,
});
}
/// Scroll the list to the given item index with an offset in number of items.
///
/// This uses non-strict scrolling: if the item is already visible within the offset region,
/// no scrolling occurs.
///
/// The offset parameter shrinks the effective viewport by the specified number of items
/// from the corresponding edge, then applies the scroll strategy within that reduced viewport:
/// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top
/// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport
/// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom
pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset,
scroll_strict: false,
});
}
/// Scroll the list so that the given item index is at the exact scroll strategy position with an offset.
///
/// This uses strict scrolling: the item will always be scrolled to match the strategy position,
/// even if it's already visible.
///
/// The offset parameter shrinks the effective viewport by the specified number of items
/// from the corresponding edge, then applies the scroll strategy within that reduced viewport:
/// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top
/// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport
/// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom
pub fn scroll_to_item_strict_with_offset(
&self,
ix: usize,
strategy: ScrollStrategy,
offset: usize,
) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset,
scroll_strict: true,
});
}
/// Check if the list is flipped vertically.
pub fn y_flipped(&self) -> bool {
self.0.borrow().y_flipped
}
/// Get the index of the topmost visible child.
#[cfg(any(test, feature = "test-support"))]
pub fn logical_scroll_top_index(&self) -> usize {
let this = self.0.borrow();
this.deferred_scroll_to_item
.as_ref()
.map(|deferred| deferred.item_index)
.unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
}
/// Checks if the list can be scrolled vertically.
pub fn is_scrollable(&self) -> bool {
if let Some(size) = self.0.borrow().last_item_size {
size.contents.height > size.item.height
} else {
false
}
}
/// Whether the list is scrolled to the end, or `None` if the list is
/// not scrollable.
pub fn is_scrolled_to_end(&self) -> Option<bool> {
let state = self.0.borrow();
let max_offset = state.base_handle.max_offset();
if max_offset.y <= px(0.) {
return None;
}
let offset = state.base_handle.offset();
Some(-offset.y >= max_offset.y)
}
/// Scroll to the bottom of the list.
pub fn scroll_to_bottom(&self) {
self.scroll_to_item(usize::MAX, ScrollStrategy::Bottom);
}
}
impl Styled for UniformList {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
impl Element for UniformList {
type RequestLayoutState = UniformListFrameState;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let max_items = self.item_count;
let item_size = self.measure_item(None, window, cx);
let layout_id = self.interactivity.request_layout(
global_id,
inspector_id,
window,
cx,
|style, window, cx| match self.sizing_behavior {
ListSizingBehavior::Infer => {
window.with_text_style(style.text_style().cloned(), |window| {
window.request_measured_layout(
style,
move |known_dimensions, available_space, _window, _cx| {
let desired_height = item_size.height * max_items;
let width = known_dimensions.width.unwrap_or(match available_space
.width
{
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_size.width
}
});
let height = match available_space.height {
AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
desired_height
}
};
size(width, height)
},
)
})
}
ListSizingBehavior::Auto => window
.with_text_style(style.text_style().cloned(), |window| {
window.request_layout(style, None, cx)
}),
},
);
(
layout_id,
UniformListFrameState {
items: SmallVec::new(),
decorations: SmallVec::new(),
},
)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
frame_state: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Option<Hitbox> {
let style = self
.interactivity
.compute_style(global_id, None, window, cx);
let border = style.border_widths.to_pixels(window.rem_size());
let padding = style
.padding
.to_pixels(bounds.size.into(), window.rem_size());
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top + padding.top),
bounds.bottom_right()
- point(border.right + padding.right, border.bottom + padding.bottom),
);
let can_scroll_horizontally = matches!(
self.horizontal_sizing_behavior,
ListHorizontalSizingBehavior::Unconstrained
);
let longest_item_size = self.measure_item(None, window, cx);
let content_width = if can_scroll_horizontally {
padded_bounds.size.width.max(longest_item_size.width)
} else {
padded_bounds.size.width
};
let content_size = Size {
width: content_width,
height: longest_item_size.height * self.item_count,
};
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
let item_height = longest_item_size.height;
let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
let mut handle = handle.0.borrow_mut();
handle.last_item_size = Some(ItemSize {
item: padded_bounds.size,
contents: content_size,
});
handle.deferred_scroll_to_item.take()
});
self.interactivity.prepaint(
global_id,
inspector_id,
bounds,
content_size,
window,
cx,
|_style, mut scroll_offset, hitbox, window, cx| {
let y_flipped = if let Some(scroll_handle) = &self.scroll_handle {
let scroll_state = scroll_handle.0.borrow();
scroll_state.y_flipped
} else {
false
};
if self.item_count > 0 {
let content_height = item_height * self.item_count;
let is_scrolled_vertically = !scroll_offset.y.is_zero();
let max_scroll_offset = padded_bounds.size.height - content_height;
if is_scrolled_vertically && scroll_offset.y < max_scroll_offset {
shared_scroll_offset.borrow_mut().y = max_scroll_offset;
scroll_offset.y = max_scroll_offset;
}
let content_width = content_size.width + padding.left + padding.right;
let is_scrolled_horizontally =
can_scroll_horizontally && !scroll_offset.x.is_zero();
if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
scroll_offset.x = Pixels::ZERO;
}
if let Some(DeferredScrollToItem {
mut item_index,
mut strategy,
offset,
scroll_strict,
}) = shared_scroll_to_item
{
if y_flipped {
item_index = self.item_count.saturating_sub(item_index + 1);
}
let list_height = padded_bounds.size.height;
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
let item_top = item_height * item_index;
let item_bottom = item_top + item_height;
let scroll_top = -updated_scroll_offset.y;
let offset_pixels = item_height * offset;
// is the selected item above/below currently visible items
let is_above = item_top < scroll_top + offset_pixels;
let is_below = item_bottom > scroll_top + list_height;
if scroll_strict || is_above || is_below {
if strategy == ScrollStrategy::Nearest {
if is_above {
strategy = ScrollStrategy::Top;
} else if is_below {
strategy = ScrollStrategy::Bottom;
}
}
let max_scroll_offset =
(content_height - list_height).max(Pixels::ZERO);
match strategy {
ScrollStrategy::Top => {
updated_scroll_offset.y = -(item_top - offset_pixels)
.clamp(Pixels::ZERO, max_scroll_offset);
}
ScrollStrategy::Center => {
let item_center = item_top + item_height / 2.0;
let viewport_height = list_height - offset_pixels;
let viewport_center = offset_pixels + viewport_height / 2.0;
let target_scroll_top = item_center - viewport_center;
updated_scroll_offset.y =
-target_scroll_top.clamp(Pixels::ZERO, max_scroll_offset);
}
ScrollStrategy::Bottom => {
updated_scroll_offset.y = -(item_bottom - list_height)
.clamp(Pixels::ZERO, max_scroll_offset);
}
ScrollStrategy::Nearest => {
// Nearest, but the item is visible -> no scroll is required
}
}
}
scroll_offset = *updated_scroll_offset
}
let first_visible_element_ix =
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
/ item_height)
.ceil() as usize;
let visible_range = first_visible_element_ix
..cmp::min(last_visible_element_ix, self.item_count);
let items = if y_flipped {
let flipped_range = self.item_count.saturating_sub(visible_range.end)
..self.item_count.saturating_sub(visible_range.start);
let mut items = (self.render_items)(flipped_range, window, cx);
items.reverse();
items
} else {
(self.render_items)(visible_range.clone(), window, cx)
};
let content_mask = ContentMask { bounds };
window.with_content_mask(Some(content_mask), |window| {
for (mut item, ix) in items.into_iter().zip(visible_range.clone()) {
let item_origin = padded_bounds.origin
+ scroll_offset
+ point(Pixels::ZERO, item_height * ix);
let available_width = if can_scroll_horizontally {
padded_bounds.size.width + scroll_offset.x.abs()
} else {
padded_bounds.size.width
};
let available_space = size(
AvailableSpace::Definite(available_width),
AvailableSpace::Definite(item_height),
);
item.layout_as_root(available_space, window, cx);
item.prepaint_at(item_origin, window, cx);
frame_state.items.push(item);
}
let bounds =
Bounds::new(padded_bounds.origin + scroll_offset, padded_bounds.size);
for decoration in &self.decorations {
let mut decoration = decoration.as_ref().compute(
visible_range.clone(),
bounds,
scroll_offset,
item_height,
self.item_count,
window,
cx,
);
let available_space = size(
AvailableSpace::Definite(bounds.size.width),
AvailableSpace::Definite(bounds.size.height),
);
decoration.layout_as_root(available_space, window, cx);
decoration.prepaint_at(bounds.origin, window, cx);
frame_state.decorations.push(decoration);
}
});
}
hitbox
},
)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<crate::Pixels>,
request_layout: &mut Self::RequestLayoutState,
hitbox: &mut Option<Hitbox>,
window: &mut Window,
cx: &mut App,
) {
self.interactivity.paint(
global_id,
inspector_id,
bounds,
hitbox.as_ref(),
window,
cx,
|_, window, cx| {
for item in &mut request_layout.items {
item.paint(window, cx);
}
for decoration in &mut request_layout.decorations {
decoration.paint(window, cx);
}
},
)
}
}
impl IntoElement for UniformList {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// A decoration for a [`UniformList`]. This can be used for various things,
/// such as rendering indent guides, or other visual effects.
pub trait UniformListDecoration {
/// Compute the decoration element, given the visible range of list items,
/// the bounds of the list, and the height of each item.
fn compute(
&self,
visible_range: Range<usize>,
bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
item_height: Pixels,
item_count: usize,
window: &mut Window,
cx: &mut App,
) -> AnyElement;
}
impl<T: UniformListDecoration + 'static> UniformListDecoration for Entity<T> {
fn compute(
&self,
visible_range: Range<usize>,
bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
item_height: Pixels,
item_count: usize,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
self.update(cx, |inner, cx| {
inner.compute(
visible_range,
bounds,
scroll_offset,
item_height,
item_count,
window,
cx,
)
})
}
}
impl UniformList {
/// Selects a specific list item for measurement.
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
self.item_to_measure_index = item_index.unwrap_or(0);
self
}
/// Sets the sizing behavior, similar to the `List` element.
pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
self.sizing_behavior = behavior;
self
}
/// Sets the horizontal sizing behavior, controlling the way list items laid out horizontally.
/// With [`ListHorizontalSizingBehavior::Unconstrained`] behavior, every item and the list itself will
/// have the size of the widest item and lay out pushing the `end_slot` to the right end.
pub fn with_horizontal_sizing_behavior(
mut self,
behavior: ListHorizontalSizingBehavior,
) -> Self {
self.horizontal_sizing_behavior = behavior;
match behavior {
ListHorizontalSizingBehavior::FitList => {
self.interactivity.base_style.overflow.x = None;
}
ListHorizontalSizingBehavior::Unconstrained => {
self.interactivity.base_style.overflow.x = Some(Overflow::Scroll);
}
}
self
}
/// Adds a decoration element to the list.
pub fn with_decoration(mut self, decoration: impl UniformListDecoration + 'static) -> Self {
self.decorations.push(Box::new(decoration));
self
}
fn measure_item(
&self,
list_width: Option<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Size<Pixels> {
if self.item_count == 0 {
return Size::default();
}
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx);
let Some(mut item_to_measure) = items.pop() else {
return Size::default();
};
let available_space = size(
list_width.map_or(AvailableSpace::MaxContent, |width| {
AvailableSpace::Definite(width)
}),
AvailableSpace::MinContent,
);
item_to_measure.layout_as_root(available_space, window, cx)
}
/// Track and render scroll state of this list with reference to the given scroll handle.
pub fn track_scroll(mut self, handle: &UniformListScrollHandle) -> Self {
self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone());
self.scroll_handle = Some(handle.clone());
self
}
/// Sets whether the list is flipped vertically, such that item 0 appears at the bottom.
pub fn y_flipped(mut self, y_flipped: bool) -> Self {
if let Some(ref scroll_handle) = self.scroll_handle {
let mut scroll_state = scroll_handle.0.borrow_mut();
let mut base_handle = &scroll_state.base_handle;
let offset = base_handle.offset();
match scroll_state.last_item_size {
Some(last_size) if scroll_state.y_flipped != y_flipped => {
let new_y_offset =
-(offset.y + last_size.contents.height - last_size.item.height);
base_handle.set_offset(point(offset.x, new_y_offset));
scroll_state.y_flipped = y_flipped;
}
// Handle case where list is initially flipped.
None if y_flipped => {
base_handle.set_offset(point(offset.x, Pixels::MIN));
scroll_state.y_flipped = y_flipped;
}
_ => {}
}
}
self
}
}
impl InteractiveElement for UniformList {
fn interactivity(&mut self) -> &mut crate::Interactivity {
&mut self.interactivity
}
}
#[cfg(test)]
mod test {
use crate::TestAppContext;
#[gpui::test]
fn test_scroll_strategy_nearest(cx: &mut TestAppContext) {
use crate::{
Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, div, prelude::*,
px, uniform_list,
};
use std::ops::Range;
actions!(example, [SelectNext, SelectPrev]);
struct TestView {
index: usize,
length: usize,
scroll_handle: UniformListScrollHandle,
focus_handle: FocusHandle,
visible_range: Range<usize>,
}
impl TestView {
pub fn select_next(
&mut self,
_: &SelectNext,
window: &mut Window,
_: &mut Context<Self>,
) {
if self.index + 1 == self.length {
self.index = 0
} else {
self.index += 1;
}
self.scroll_handle
.scroll_to_item(self.index, ScrollStrategy::Nearest);
window.refresh();
}
pub fn select_previous(
&mut self,
_: &SelectPrev,
window: &mut Window,
_: &mut Context<Self>,
) {
if self.index == 0 {
self.index = self.length - 1
} else {
self.index -= 1;
}
self.scroll_handle
.scroll_to_item(self.index, ScrollStrategy::Nearest);
window.refresh();
}
}
impl Render for TestView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("list-example")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.size_full()
.child(
uniform_list(
"entries",
self.length,
cx.processor(|this, range: Range<usize>, _window, _cx| {
this.visible_range = range.clone();
range
.map(|ix| div().id(ix).h(px(20.0)).child(format!("Item {ix}")))
.collect()
}),
)
.track_scroll(&self.scroll_handle)
.h(px(200.0)),
)
}
}
let (view, cx) = cx.add_window_view(|window, cx| {
let focus_handle = cx.focus_handle();
window.focus(&focus_handle, cx);
TestView {
scroll_handle: UniformListScrollHandle::new(),
index: 0,
focus_handle,
length: 47,
visible_range: 0..0,
}
});
// 10 out of 47 items are visible
// First 9 times selecting next item does not scroll
for ix in 1..10 {
cx.dispatch_action(SelectNext);
view.read_with(cx, |view, _| {
assert_eq!(view.index, ix);
assert_eq!(view.visible_range, 0..10);
})
}
// Now each time the list scrolls down by 1
for ix in 10..47 {
cx.dispatch_action(SelectNext);
view.read_with(cx, |view, _| {
assert_eq!(view.index, ix);
assert_eq!(view.visible_range, ix - 9..ix + 1);
})
}
// After the last item we move back to the start
cx.dispatch_action(SelectNext);
view.read_with(cx, |view, _| {
assert_eq!(view.index, 0);
assert_eq!(view.visible_range, 0..10);
});
// Return to the last element
cx.dispatch_action(SelectPrev);
view.read_with(cx, |view, _| {
assert_eq!(view.index, 46);
assert_eq!(view.visible_range, 37..47);
});
// First 9 times selecting previous does not scroll
for ix in (37..46).rev() {
cx.dispatch_action(SelectPrev);
view.read_with(cx, |view, _| {
assert_eq!(view.index, ix);
assert_eq!(view.visible_range, 37..47);
})
}
// Now each time the list scrolls up by 1
for ix in (0..37).rev() {
cx.dispatch_action(SelectPrev);
view.read_with(cx, |view, _| {
assert_eq!(view.index, ix);
assert_eq!(view.visible_range, ix..ix + 10);
})
}
}
}