Present Servo BGRA hardware surfaces
This commit is contained in:
+292
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+3250
File diff suppressed because it is too large
Load Diff
+353
@@ -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()
|
||||
}
|
||||
}
|
||||
Vendored
+767
@@ -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))
|
||||
}
|
||||
}
|
||||
+1287
File diff suppressed because it is too large
Load Diff
Vendored
+25
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+221
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user