Present Servo BGRA hardware surfaces

This commit is contained in:
2026-05-13 00:22:00 -04:00
parent bf1ebfb6fe
commit 05a7a0d67f
154 changed files with 84157 additions and 115 deletions
+292
View File
@@ -0,0 +1,292 @@
use smallvec::SmallVec;
use crate::{
AnyElement, App, Axis, Bounds, Corner, 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_corner: Corner,
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_corner: Corner::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: Corner) -> Self {
self.anchor_corner = 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 mut child_min = point(Pixels::MAX, Pixels::MAX);
let mut child_max = Point::default();
for child_layout_id in &request_layout.child_layout_ids {
let child_bounds = window.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.bottom_right());
}
let size: Size<Pixels> = (child_max - child_min).into();
let (origin, mut desired) = self.position_mode.get_position_and_bounds(
self.anchor_position,
self.anchor_corner,
size,
bounds,
self.offset,
);
let limits = Bounds {
origin: Point::default(),
size: window.viewport_size(),
};
if self.fit_mode == AnchoredFitMode::SwitchAnchor {
let mut anchor_corner = self.anchor_corner;
if desired.left() < limits.left() || desired.right() > limits.right() {
let switched = Bounds::from_corner_and_size(
anchor_corner.other_side_corner_along(Axis::Horizontal),
origin,
size,
);
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
anchor_corner = anchor_corner.other_side_corner_along(Axis::Horizontal);
desired = switched
}
}
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
let switched = Bounds::from_corner_and_size(
anchor_corner.other_side_corner_along(Axis::Vertical),
origin,
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_corner: Corner,
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_corner_and_size(anchor_corner, anchor_position + offset, size);
(anchor_position, bounds)
}
AnchoredPositionMode::Local => {
let anchor_position = anchor_position.unwrap_or_default();
let bounds = Bounds::from_corner_and_size(
anchor_corner,
bounds.origin + anchor_position + offset,
size,
);
(anchor_position, bounds)
}
}
}
}
+263
View File
@@ -0,0 +1,263 @@
use std::{
rc::Rc,
time::{Duration, Instant},
};
use crate::{
AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Window,
};
pub use easing::*;
use smallvec::SmallVec;
/// An animation that can be applied to an element.
#[derive(Clone)]
pub struct Animation {
/// The amount of time for which this animation should run
pub duration: Duration,
/// Whether to repeat this animation when it finishes
pub oneshot: bool,
/// A function that takes a delta between 0 and 1 and returns a new delta
/// between 0 and 1 based on the given easing function.
pub easing: Rc<dyn Fn(f32) -> f32>,
}
impl Animation {
/// Create a new animation with the given duration.
/// By default the animation will only run once and will use a linear easing function.
pub fn new(duration: Duration) -> Self {
Self {
duration,
oneshot: true,
easing: Rc::new(linear),
}
}
/// Set the animation to loop when it finishes.
pub fn repeat(mut self) -> Self {
self.oneshot = false;
self
}
/// Set the easing function to use for this animation.
/// The easing function will take a time delta between 0 and 1 and return a new delta
/// between 0 and 1
pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
self.easing = Rc::new(easing);
self
}
}
/// An extension trait for adding the animation wrapper to both Elements and Components
pub trait AnimationExt {
/// Render this component or element with an animation
fn with_animation(
self,
id: impl Into<ElementId>,
animation: Animation,
animator: impl Fn(Self, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(move |this, _, value| animator(this, value)),
animations: smallvec::smallvec![animation],
}
}
/// Render this component or element with a chain of animations
fn with_animations(
self,
id: impl Into<ElementId>,
animations: Vec<Animation>,
animator: impl Fn(Self, usize, f32) -> Self + 'static,
) -> AnimationElement<Self>
where
Self: Sized,
{
AnimationElement {
id: id.into(),
element: Some(self),
animator: Box::new(animator),
animations: animations.into(),
}
}
}
impl<E: IntoElement + 'static> AnimationExt for E {}
/// A GPUI element that applies an animation to another element
pub struct AnimationElement<E> {
id: ElementId,
element: Option<E>,
animations: SmallVec<[Animation; 1]>,
animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
}
impl<E> AnimationElement<E> {
/// Returns a new [`AnimationElement<E>`] after applying the given function
/// to the element being animated.
pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
self.element = self.element.map(f);
self
}
}
impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
type Element = AnimationElement<E>;
fn into_element(self) -> Self::Element {
self
}
}
struct AnimationState {
start: Instant,
animation_ix: usize,
}
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.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,
) -> (crate::LayoutId, Self::RequestLayoutState) {
window.with_element_state(global_id.unwrap(), |state, window| {
let mut state = state.unwrap_or_else(|| AnimationState {
start: Instant::now(),
animation_ix: 0,
});
let animation_ix = state.animation_ix;
let mut delta = state.start.elapsed().as_secs_f32()
/ self.animations[animation_ix].duration.as_secs_f32();
let mut done = false;
if delta > 1.0 {
if self.animations[animation_ix].oneshot {
if animation_ix >= self.animations.len() - 1 {
done = true;
} else {
state.start = Instant::now();
state.animation_ix += 1;
}
delta = 1.0;
} else {
delta %= 1.0;
}
}
let delta = (self.animations[animation_ix].easing)(delta);
debug_assert!(
(0.0..=1.0).contains(&delta),
"delta should always be between 0 and 1"
);
let element = self.element.take().expect("should only be called once");
let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
if !done {
window.request_animation_frame();
}
((element.request_layout(window, cx), element), state)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
element.prepaint(window, cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx);
}
}
mod easing {
use std::f32::consts::PI;
/// The linear easing function, or delta itself
pub fn linear(delta: f32) -> f32 {
delta
}
/// The quadratic easing function, delta * delta
pub fn quadratic(delta: f32) -> f32 {
delta * delta
}
/// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
pub fn ease_in_out(delta: f32) -> f32 {
if delta < 0.5 {
2.0 * delta * delta
} else {
let x = -2.0 * delta + 2.0;
1.0 - x * x / 2.0
}
}
/// The Quint ease-out function, which starts quickly and decelerates to a stop
pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
move |delta| 1.0 - (1.0 - delta).powi(5)
}
/// Apply the given easing function, first in the forward direction and then in the reverse direction
pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
move |delta| {
if delta < 0.5 {
easing(delta * 2.0)
} else {
easing((1.0 - delta) * 2.0)
}
}
}
/// A custom easing function for pulsating alpha that slows down as it approaches 0.1
pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
let range = max - min;
move |delta| {
// Use a combination of sine and cubic functions for a more natural breathing rhythm
let t = (delta * 2.0 * PI).sin();
let breath = (t * t * t + t) / 2.0;
// Map the breath to our desired alpha range
let normalized_alpha = (breath + 1.0) / 2.0;
min + (normalized_alpha * range)
}
}
}
+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
}
}
+96
View File
@@ -0,0 +1,96 @@
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)
}
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
}
}
File diff suppressed because it is too large Load Diff
+353
View File
@@ -0,0 +1,353 @@
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, Task, Window,
hash,
};
use futures::{FutureExt, future::Shared};
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);
}
})
}
}
/// An image loading task associated with an image cache.
pub type ImageLoadingTask = Shared<Task<Result<Arc<RenderImage>, ImageCacheError>>>;
/// An image cache item
pub enum ImageCacheItem {
/// The associated image is currently loading
Loading(ImageLoadingTask),
/// This item has loaded an image.
Loaded(Result<Arc<RenderImage>, ImageCacheError>),
}
impl std::fmt::Debug for ImageCacheItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = match self {
ImageCacheItem::Loading(_) => &"Loading...".to_string(),
ImageCacheItem::Loaded(render_image) => &format!("{:?}", render_image),
};
f.debug_struct("ImageCacheItem")
.field("status", status)
.finish()
}
}
impl ImageCacheItem {
/// Attempt to get the image from the cache item.
pub fn get(&mut self) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
ImageCacheItem::Loading(task) => {
let res = task.now_or_never()?;
*self = ImageCacheItem::Loaded(res.clone());
Some(res)
}
ImageCacheItem::Loaded(res) => Some(res.clone()),
}
}
}
/// 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 (_, mut 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);
if let Some(item) = self.0.get_mut(&hash) {
return item.get();
}
let fut = AssetLogger::<ImageAssetLoader>::load(source.clone(), cx);
let task = cx.background_executor().spawn(fut).shared();
self.0.insert(hash, ImageCacheItem::Loading(task.clone()));
let entity = window.current_view();
window
.spawn(cx, {
async move |cx| {
_ = task.await;
cx.on_next_frame(move |_, cx| {
cx.notify(entity);
});
}
})
.detach();
None
}
/// Clear the image cache.
pub fn clear(&mut self, window: &mut Window, cx: &mut App) {
for (_, mut 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(mut 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()
}
}
+767
View File
@@ -0,0 +1,767 @@
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,
SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task,
Window, px, swap_rgba_pa_to_bgra,
};
use anyhow::{Context as _, Result};
use futures::{AsyncReadExt, Future};
use image::{
AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba,
codecs::{gif::GifDecoder, webp::WebPDecoder},
};
use smallvec::SmallVec;
use std::{
fs,
io::{self, Cursor},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};
use thiserror::Error;
use util::ResultExt;
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 {
http_client::Uri::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 the object fit for the image.
fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().fallback = Some(Box::new(fallback));
self
}
/// Set the object fit for the image.
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 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)) => {
if let Some(state) = &mut state {
let frame_count = data.frame_count();
if frame_count > 1 {
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);
}
}
state.started_loading = None;
}
let image_size = data.render_size(frame_index);
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.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,
) {
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())
.clamp_radii_for_quad_size(new_bounds.size);
window
.paint_image(
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),
}
}
}
#[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) => {
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(),
));
}
}
};
let data = 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() {
let mut frame = frame?;
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
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() {
let mut frame = frame?;
// Convert from RGBA to BGRA.
for pixel in frame.buffer_mut().chunks_exact_mut(4) {
pixel.swap(0, 2);
}
frames.push(frame);
}
frames
} else {
let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
}
_ => {
let mut data =
image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
SmallVec::from_elem(Frame::new(data), 1)
}
};
RenderImage::new(data)
} else {
let pixmap =
// TODO: Can we make svgs always rescale?
svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
let mut buffer =
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
for pixel in buffer.chunks_exact_mut(4) {
swap_rgba_pa_to_bgra(pixel);
}
let mut image = RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1));
image.scale_factor = SMOOTH_SVG_SCALE_FACTOR;
image
};
Ok(Arc::new(data))
}
}
}
/// 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))
}
}
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
mod anchored;
mod animation;
mod canvas;
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 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::*;
+120
View File
@@ -0,0 +1,120 @@
use crate::{
App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
ObjectFit, Pixels, Style, StyleRefinement, Styled, Window,
};
#[cfg(target_os = "macos")]
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 macOS image buffer from CoreVideo
#[cfg(target_os = "macos")]
Surface(CVPixelBuffer),
}
#[cfg(target_os = "macos")]
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.
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(target_os = "macos"), allow(unused_variables))] bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))] window: &mut Window,
_: &mut App,
) {
match &self.source {
#[cfg(target_os = "macos")]
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
}
}
+221
View File
@@ -0,0 +1,221 @@
use crate::{
App, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
radians, size,
};
use util::ResultExt;
/// An SVG element.
pub struct Svg {
interactivity: Interactivity,
transformation: Option<Transformation>,
path: Option<SharedString>,
}
/// Create a new SVG element.
#[track_caller]
pub fn svg() -> Svg {
Svg {
interactivity: Interactivity::new(),
transformation: None,
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
}
/// 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| {
if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
let transformation = self
.transformation
.as_ref()
.map(|transformation| {
transformation.into_matrix(bounds.center(), window.scale_factor())
})
.unwrap_or_default();
window
.paint_svg(bounds, path.clone(), 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).negate())
}
}
+914
View File
@@ -0,0 +1,914 @@
use crate::{
ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
register_tooltip_mouse_handlers, set_tooltip_on_window,
};
use anyhow::Context as _;
use smallvec::SmallVec;
use std::{
cell::{Cell, RefCell},
mem,
ops::Range,
rc::Rc,
sync::Arc,
};
use util::ResultExt;
impl Element for &'static str {
type RequestLayoutState = TextLayout;
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 mut state = TextLayout::default();
let layout_id = state.layout(SharedString::from(*self), None, window, cx);
(layout_id, state)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) {
text_layout.prepaint(bounds, self)
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
text_layout: &mut TextLayout,
_: &mut (),
window: &mut Window,
cx: &mut App,
) {
text_layout.paint(self, window, cx)
}
}
impl IntoElement for &'static str {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl IntoElement for String {
type Element = SharedString;
fn into_element(self) -> Self::Element {
self.into()
}
}
impl Element for SharedString {
type RequestLayoutState = TextLayout;
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 mut state = TextLayout::default();
let layout_id = state.layout(self.clone(), None, window, cx);
(layout_id, state)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) {
text_layout.prepaint(bounds, self.as_ref())
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
text_layout.paint(self.as_ref(), window, cx)
}
}
impl IntoElement for SharedString {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// Renders text with runs of different styles.
///
/// Callers are responsible for setting the correct style for each run.
/// For text with a uniform style, you can usually avoid calling this constructor
/// and just pass text directly.
pub struct StyledText {
text: SharedString,
runs: Option<Vec<TextRun>>,
delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
layout: TextLayout,
}
impl StyledText {
/// Construct a new styled text element from the given string.
pub fn new(text: impl Into<SharedString>) -> Self {
StyledText {
text: text.into(),
runs: None,
delayed_highlights: None,
layout: TextLayout::default(),
}
}
/// Get the layout for this element. This can be used to map indices to pixels and vice versa.
pub fn layout(&self) -> &TextLayout {
&self.layout
}
/// Set the styling attributes for the given text, as well as
/// as any ranges of text that have had their style customized.
pub fn with_default_highlights(
mut self,
default_style: &TextStyle,
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
) -> Self {
debug_assert!(
self.delayed_highlights.is_none(),
"Can't use `with_default_highlights` and `with_highlights`"
);
let runs = Self::compute_runs(&self.text, default_style, highlights);
self.with_runs(runs)
}
/// Set the styling attributes for the given text, as well as
/// as any ranges of text that have had their style customized.
pub fn with_highlights(
mut self,
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
) -> Self {
debug_assert!(
self.runs.is_none(),
"Can't use `with_highlights` and `with_default_highlights`"
);
self.delayed_highlights = Some(
highlights
.into_iter()
.inspect(|(run, _)| {
debug_assert!(self.text.is_char_boundary(run.start));
debug_assert!(self.text.is_char_boundary(run.end));
})
.collect::<Vec<_>>(),
);
self
}
fn compute_runs(
text: &str,
default_style: &TextStyle,
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
) -> Vec<TextRun> {
let mut runs = Vec::new();
let mut ix = 0;
for (range, highlight) in highlights {
if ix < range.start {
debug_assert!(text.is_char_boundary(range.start));
runs.push(default_style.clone().to_run(range.start - ix));
}
debug_assert!(text.is_char_boundary(range.end));
runs.push(
default_style
.clone()
.highlight(highlight)
.to_run(range.len()),
);
ix = range.end;
}
if ix < text.len() {
runs.push(default_style.to_run(text.len() - ix));
}
runs
}
/// Set the text runs for this piece of text.
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
let mut text = &**self.text;
for run in &runs {
text = text.get(run.len..).expect("invalid text run");
}
assert!(text.is_empty(), "invalid text run");
self.runs = Some(runs);
self
}
}
impl Element for StyledText {
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,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let runs = self.runs.take().or_else(|| {
self.delayed_highlights.take().map(|delayed_highlights| {
Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
})
});
let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_window: &mut Window,
_cx: &mut App,
) {
self.layout.prepaint(bounds, &self.text)
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.layout.paint(&self.text, window, cx)
}
}
impl IntoElement for StyledText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
#[derive(Default, Clone)]
pub struct TextLayout(Rc<RefCell<Option<TextLayoutInner>>>);
struct TextLayoutInner {
len: usize,
lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels,
wrap_width: Option<Pixels>,
size: Option<Size<Pixels>>,
bounds: Option<Bounds<Pixels>>,
}
impl TextLayout {
fn layout(
&self,
text: SharedString,
runs: Option<Vec<TextRun>>,
window: &mut Window,
_: &mut App,
) -> LayoutId {
let text_style = window.text_style();
let font_size = text_style.font_size.to_pixels(window.rem_size());
let line_height = text_style
.line_height
.to_pixels(font_size.into(), window.rem_size());
let mut runs = if let Some(runs) = runs {
runs
} else {
vec![text_style.to_run(text.len())]
};
window.request_measured_layout(Default::default(), {
let element_state = self.clone();
move |known_dimensions, available_space, window, cx| {
let wrap_width = if text_style.white_space == WhiteSpace::Normal {
known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => Some(x),
_ => None,
})
} else {
None
};
let (truncate_width, truncation_suffix) =
if let Some(text_overflow) = text_style.text_overflow.clone() {
let width = known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
Some(max_lines) => Some(x * max_lines),
None => Some(x),
},
_ => None,
});
match text_overflow {
TextOverflow::Truncate(s) => (width, s),
}
} else {
(None, "".into())
};
if let Some(text_layout) = element_state.0.borrow().as_ref()
&& text_layout.size.is_some()
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
{
return text_layout.size.unwrap();
}
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
let text = if let Some(truncate_width) = truncate_width {
line_wrapper.truncate_line(
text.clone(),
truncate_width,
&truncation_suffix,
&mut runs,
)
} else {
text.clone()
};
let len = text.len();
let Some(lines) = window
.text_system()
.shape_text(
text,
font_size,
&runs,
wrap_width, // Wrap if we know the width.
text_style.line_clamp, // Limit the number of lines if line_clamp is set.
)
.log_err()
else {
element_state.0.borrow_mut().replace(TextLayoutInner {
lines: Default::default(),
len: 0,
line_height,
wrap_width,
size: Some(Size::default()),
bounds: None,
});
return Size::default();
};
let mut size: Size<Pixels> = Size::default();
for line in &lines {
let line_size = line.size(line_height);
size.height += line_size.height;
size.width = size.width.max(line_size.width).ceil();
}
element_state.0.borrow_mut().replace(TextLayoutInner {
lines,
len,
line_height,
wrap_width,
size: Some(size),
bounds: None,
});
size
}
})
}
fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
let mut element_state = self.0.borrow_mut();
let element_state = element_state
.as_mut()
.with_context(|| format!("measurement has not been performed on {text}"))
.unwrap();
element_state.bounds = Some(bounds);
}
fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
let element_state = self.0.borrow();
let element_state = element_state
.as_ref()
.with_context(|| format!("measurement has not been performed on {text}"))
.unwrap();
let bounds = element_state
.bounds
.with_context(|| format!("prepaint has not been performed on {text}"))
.unwrap();
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let text_style = window.text_style();
for line in &element_state.lines {
line.paint_background(
line_origin,
line_height,
text_style.text_align,
Some(bounds),
window,
cx,
)
.log_err();
line.paint(
line_origin,
line_height,
text_style.text_align,
Some(bounds),
window,
cx,
)
.log_err();
line_origin.y += line.size(line_height).height;
}
}
/// Get the byte index into the input of the pixel position.
pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
let element_state = self.0.borrow();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
if position.y < bounds.top() {
return Err(0);
}
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_bottom = line_origin.y + line.size(line_height).height;
if position.y > line_bottom {
line_origin.y = line_bottom;
line_start_ix += line.len() + 1;
} else {
let position_within_line = position - line_origin;
match line.index_for_position(position_within_line, line_height) {
Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
Err(index_within_line) => return Err(line_start_ix + index_within_line),
}
}
}
Err(line_start_ix.saturating_sub(1))
}
/// Get the pixel position for the given byte index.
pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
let element_state = self.0.borrow();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_end_ix = line_start_ix + line.len();
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_origin.y += line.size(line_height).height;
line_start_ix = line_end_ix + 1;
continue;
} else {
let ix_within_line = index - line_start_ix;
return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
}
}
None
}
/// Retrieve the layout for the line containing the given byte index.
pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
let element_state = self.0.borrow();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_end_ix = line_start_ix + line.len();
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_origin.y += line.size(line_height).height;
line_start_ix = line_end_ix + 1;
continue;
} else {
return Some(line.layout.clone());
}
}
None
}
/// The bounds of this layout.
pub fn bounds(&self) -> Bounds<Pixels> {
self.0.borrow().as_ref().unwrap().bounds.unwrap()
}
/// The line height for this layout.
pub fn line_height(&self) -> Pixels {
self.0.borrow().as_ref().unwrap().line_height
}
/// The UTF-8 length of the underlying text.
pub fn len(&self) -> usize {
self.0.borrow().as_ref().unwrap().len
}
/// The text for this layout.
pub fn text(&self) -> String {
self.0
.borrow()
.as_ref()
.unwrap()
.lines
.iter()
.map(|s| s.text.to_string())
.collect::<Vec<_>>()
.join("\n")
}
/// The text for this layout (with soft-wraps as newlines)
pub fn wrapped_text(&self) -> String {
let mut lines = Vec::new();
for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
let mut seen = 0;
for boundary in wrapped.layout.wrap_boundaries.iter() {
let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
[boundary.glyph_ix]
.index;
lines.push(wrapped.text[seen..index].to_string());
seen = index;
}
lines.push(wrapped.text[seen..].to_string());
}
lines.join("\n")
}
}
/// A text element that can be interacted with.
pub struct InteractiveText {
element_id: ElementId,
text: StyledText,
click_listener:
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
tooltip_id: Option<TooltipId>,
clickable_ranges: Vec<Range<usize>>,
}
struct InteractiveTextClickEvent {
mouse_down_index: usize,
mouse_up_index: usize,
}
#[doc(hidden)]
#[derive(Default)]
pub struct InteractiveTextState {
mouse_down_index: Rc<Cell<Option<usize>>>,
hovered_index: Rc<Cell<Option<usize>>>,
active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
}
/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
impl InteractiveText {
/// Creates a new InteractiveText from the given text.
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
Self {
element_id: id.into(),
text,
click_listener: None,
hover_listener: None,
tooltip_builder: None,
tooltip_id: None,
clickable_ranges: Vec::new(),
}
}
/// on_click is called when the user clicks on one of the given ranges, passing the index of
/// the clicked range.
pub fn on_click(
mut self,
ranges: Vec<Range<usize>>,
listener: impl Fn(usize, &mut Window, &mut App) + 'static,
) -> Self {
self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
for (range_ix, range) in ranges.iter().enumerate() {
if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
{
listener(range_ix, window, cx);
}
}
}));
self.clickable_ranges = ranges;
self
}
/// on_hover is called when the mouse moves over a character within the text, passing the
/// index of the hovered character, or None if the mouse leaves the text.
pub fn on_hover(
mut self,
listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.hover_listener = Some(Box::new(listener));
self
}
/// tooltip lets you specify a tooltip for a given character index in the string.
pub fn tooltip(
mut self,
builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
) -> Self {
self.tooltip_builder = Some(Rc::new(builder));
self
}
}
impl Element for InteractiveText {
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> {
Some(self.element_id.clone())
}
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) {
self.text.request_layout(None, inspector_id, window, cx)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Hitbox {
window.with_optional_element_state::<InteractiveTextState, _>(
global_id,
|interactive_state, window| {
let mut interactive_state = interactive_state
.map(|interactive_state| interactive_state.unwrap_or_default());
if let Some(interactive_state) = interactive_state.as_mut() {
if self.tooltip_builder.is_some() {
self.tooltip_id =
set_tooltip_on_window(&interactive_state.active_tooltip, window);
} else {
// If there is no longer a tooltip builder, remove the active tooltip.
interactive_state.active_tooltip.take();
}
}
self.text
.prepaint(None, inspector_id, bounds, state, window, cx);
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
(hitbox, interactive_state)
},
)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App,
) {
let current_view = window.current_view();
let text_layout = self.text.layout().clone();
window.with_element_state::<InteractiveTextState, _>(
global_id.unwrap(),
|interactive_state, window| {
let mut interactive_state = interactive_state.unwrap_or_default();
if let Some(click_listener) = self.click_listener.take() {
let mouse_position = window.mouse_position();
if let Ok(ix) = text_layout.index_for_position(mouse_position)
&& self
.clickable_ranges
.iter()
.any(|range| range.contains(&ix))
{
window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
}
let text_layout = text_layout.clone();
let mouse_down = interactive_state.mouse_down_index.clone();
if let Some(mouse_down_index) = mouse_down.get() {
let hitbox = hitbox.clone();
let clickable_ranges = mem::take(&mut self.clickable_ranges);
window.on_mouse_event(
move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
if let Ok(mouse_up_index) =
text_layout.index_for_position(event.position)
{
click_listener(
&clickable_ranges,
InteractiveTextClickEvent {
mouse_down_index,
mouse_up_index,
},
window,
cx,
)
}
mouse_down.take();
window.refresh();
}
},
);
} else {
let hitbox = hitbox.clone();
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
if phase == DispatchPhase::Bubble
&& hitbox.is_hovered(window)
&& let Ok(mouse_down_index) =
text_layout.index_for_position(event.position)
{
mouse_down.set(Some(mouse_down_index));
window.refresh();
}
});
}
}
window.on_mouse_event({
let mut hover_listener = self.hover_listener.take();
let hitbox = hitbox.clone();
let text_layout = text_layout.clone();
let hovered_index = interactive_state.hovered_index.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
let current = hovered_index.get();
let updated = text_layout.index_for_position(event.position).ok();
if current != updated {
hovered_index.set(updated);
if let Some(hover_listener) = hover_listener.as_ref() {
hover_listener(updated, event.clone(), window, cx);
}
cx.notify(current_view);
}
}
}
});
if let Some(tooltip_builder) = self.tooltip_builder.clone() {
let active_tooltip = interactive_state.active_tooltip.clone();
let build_tooltip = Rc::new({
let tooltip_is_hoverable = false;
let text_layout = text_layout.clone();
move |window: &mut Window, cx: &mut App| {
text_layout
.index_for_position(window.mouse_position())
.ok()
.and_then(|position| tooltip_builder(position, window, cx))
.map(|view| (view, tooltip_is_hoverable))
}
});
// Use bounds instead of testing hitbox since this is called during prepaint.
let check_is_hovered_during_prepaint = Rc::new({
let source_bounds = hitbox.bounds;
let text_layout = text_layout.clone();
let pending_mouse_down = interactive_state.mouse_down_index.clone();
move |window: &Window| {
text_layout
.index_for_position(window.mouse_position())
.is_ok()
&& source_bounds.contains(&window.mouse_position())
&& pending_mouse_down.get().is_none()
}
});
let check_is_hovered = Rc::new({
let hitbox = hitbox.clone();
let text_layout = text_layout.clone();
let pending_mouse_down = interactive_state.mouse_down_index.clone();
move |window: &Window| {
text_layout
.index_for_position(window.mouse_position())
.is_ok()
&& hitbox.is_hovered(window)
&& pending_mouse_down.get().is_none()
}
});
register_tooltip_mouse_handlers(
&active_tooltip,
self.tooltip_id,
build_tooltip,
check_is_hovered,
check_is_hovered_during_prepaint,
window,
);
}
self.text
.paint(None, inspector_id, bounds, &mut (), &mut (), window, cx);
((), interactive_state)
},
);
}
}
impl IntoElement for InteractiveText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
+710
View File
@@ -0,0 +1,710 @@
//! 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, 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,
}
#[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
}
}
}
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 + padding.top + padding.bottom,
};
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 + padding.top + padding.bottom;
let is_scrolled_vertically = !scroll_offset.y.is_zero();
let min_vertical_scroll_offset = padded_bounds.size.height - content_height;
if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset {
shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset;
scroll_offset.y = min_vertical_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(deferred_scroll) = shared_scroll_to_item {
let mut ix = deferred_scroll.item_index;
if y_flipped {
ix = self.item_count.saturating_sub(ix + 1);
}
let list_height = padded_bounds.size.height;
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
let item_top = item_height * ix + padding.top;
let item_bottom = item_top + item_height;
let scroll_top = -updated_scroll_offset.y;
let offset_pixels = item_height * deferred_scroll.offset;
let mut scrolled_to_top = false;
if item_top < scroll_top + padding.top + offset_pixels {
scrolled_to_top = true;
updated_scroll_offset.y = -(item_top) + padding.top + offset_pixels;
} else if item_bottom > scroll_top + list_height - padding.bottom {
scrolled_to_top = true;
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
}
if deferred_scroll.scroll_strict
|| (scrolled_to_top
&& (item_top < scroll_top + offset_pixels
|| item_bottom > scroll_top + list_height))
{
match deferred_scroll.strategy {
ScrollStrategy::Top => {
updated_scroll_offset.y = -(item_top - offset_pixels)
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
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
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
ScrollStrategy::Bottom => {
updated_scroll_offset.y = -(item_bottom - list_height
+ offset_pixels)
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
}
}
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
+ point(
if can_scroll_horizontally {
scroll_offset.x + padding.left
} else {
scroll_offset.x
},
item_height * ix + scroll_offset.y + padding.top,
);
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
+ point(
if can_scroll_horizontally {
scroll_offset.x + padding.left
} else {
scroll_offset.x
},
scroll_offset.y + padding.top,
),
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::MinContent, |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);
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
}
}