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