Move all crates to a top-level crates folder

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Nathan Sobo 2021-10-04 13:22:21 -06:00
parent d768224182
commit fdfed3d7db
282 changed files with 195588 additions and 16 deletions

View file

@ -0,0 +1,105 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
use json::ToJson;
use serde_json::json;
pub struct Align {
child: ElementBox,
alignment: Vector2F,
}
impl Align {
pub fn new(child: ElementBox) -> Self {
Self {
child,
alignment: Vector2F::zero(),
}
}
pub fn top(mut self) -> Self {
self.alignment.set_y(-1.0);
self
}
pub fn left(mut self) -> Self {
self.alignment.set_x(-1.0);
self
}
pub fn right(mut self) -> Self {
self.alignment.set_x(1.0);
self
}
}
impl Element for Align {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.max;
constraint.min = Vector2F::zero();
let child_size = self.child.layout(constraint, cx);
if size.x().is_infinite() {
size.set_x(child_size.x());
}
if size.y().is_infinite() {
size.set_y(child_size.y());
}
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let my_center = bounds.size() / 2.;
let my_target = my_center + my_center * self.alignment;
let child_center = self.child.size() / 2.;
let child_target = child_center + child_center * self.alignment;
self.child.paint(
bounds.origin() - (child_target - my_target),
visible_bounds,
cx,
);
}
fn dispatch_event(
&mut self,
event: &Event,
_: pathfinder_geometry::rect::RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
bounds: pathfinder_geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> json::Value {
json!({
"type": "Align",
"bounds": bounds.to_json(),
"alignment": self.alignment.to_json(),
"child": self.child.debug(cx),
})
}
}

View file

@ -0,0 +1,78 @@
use super::Element;
use crate::{
json::{self, json},
DebugContext, PaintContext,
};
use json::ToJson;
use pathfinder_geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
};
pub struct Canvas<F>(F);
impl<F> Canvas<F>
where
F: FnMut(RectF, RectF, &mut PaintContext),
{
pub fn new(f: F) -> Self {
Self(f)
}
}
impl<F> Element for Canvas<F>
where
F: FnMut(RectF, RectF, &mut PaintContext),
{
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: crate::SizeConstraint,
_: &mut crate::LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let x = if constraint.max.x().is_finite() {
constraint.max.x()
} else {
constraint.min.x()
};
let y = if constraint.max.y().is_finite() {
constraint.max.y()
} else {
constraint.min.y()
};
(vec2f(x, y), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.0(bounds, visible_bounds, cx)
}
fn dispatch_event(
&mut self,
_: &crate::Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut crate::EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> json::Value {
json!({"type": "Canvas", "bounds": bounds.to_json()})
}
}

View file

@ -0,0 +1,100 @@
use json::ToJson;
use serde_json::json;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
pub struct ConstrainedBox {
child: ElementBox,
constraint: SizeConstraint,
}
impl ConstrainedBox {
pub fn new(child: ElementBox) -> Self {
Self {
child,
constraint: SizeConstraint {
min: Vector2F::zero(),
max: Vector2F::splat(f32::INFINITY),
},
}
}
pub fn with_min_width(mut self, min_width: f32) -> Self {
self.constraint.min.set_x(min_width);
self
}
pub fn with_max_width(mut self, max_width: f32) -> Self {
self.constraint.max.set_x(max_width);
self
}
pub fn with_max_height(mut self, max_height: f32) -> Self {
self.constraint.max.set_y(max_height);
self
}
pub fn with_width(mut self, width: f32) -> Self {
self.constraint.min.set_x(width);
self.constraint.max.set_x(width);
self
}
pub fn with_height(mut self, height: f32) -> Self {
self.constraint.min.set_y(height);
self.constraint.max.set_y(height);
self
}
}
impl Element for ConstrainedBox {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
constraint.min = constraint.min.max(self.constraint.min);
constraint.max = constraint.max.min(self.constraint.max);
constraint.max = constraint.max.max(constraint.min);
let size = self.child.layout(constraint, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> json::Value {
json!({"type": "ConstrainedBox", "set_constraint": self.constraint.to_json(), "child": self.child.debug(cx)})
}
}

View file

@ -0,0 +1,435 @@
use pathfinder_geometry::rect::RectF;
use serde::Deserialize;
use serde_json::json;
use crate::{
color::Color,
geometry::{
deserialize_vec2f,
vector::{vec2f, Vector2F},
},
json::ToJson,
scene::{self, Border, Quad},
Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
#[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct ContainerStyle {
#[serde(default)]
pub margin: Margin,
#[serde(default)]
pub padding: Padding,
#[serde(rename = "background")]
pub background_color: Option<Color>,
#[serde(default)]
pub border: Border,
#[serde(default)]
pub corner_radius: f32,
#[serde(default)]
pub shadow: Option<Shadow>,
}
pub struct Container {
child: ElementBox,
style: ContainerStyle,
}
impl Container {
pub fn new(child: ElementBox) -> Self {
Self {
child,
style: Default::default(),
}
}
pub fn with_style(mut self, style: ContainerStyle) -> Self {
self.style = style;
self
}
pub fn with_margin_top(mut self, margin: f32) -> Self {
self.style.margin.top = margin;
self
}
pub fn with_margin_left(mut self, margin: f32) -> Self {
self.style.margin.left = margin;
self
}
pub fn with_margin_right(mut self, margin: f32) -> Self {
self.style.margin.right = margin;
self
}
pub fn with_horizontal_padding(mut self, padding: f32) -> Self {
self.style.padding.left = padding;
self.style.padding.right = padding;
self
}
pub fn with_vertical_padding(mut self, padding: f32) -> Self {
self.style.padding.top = padding;
self.style.padding.bottom = padding;
self
}
pub fn with_uniform_padding(mut self, padding: f32) -> Self {
self.style.padding = Padding {
top: padding,
left: padding,
bottom: padding,
right: padding,
};
self
}
pub fn with_padding_left(mut self, padding: f32) -> Self {
self.style.padding.left = padding;
self
}
pub fn with_padding_right(mut self, padding: f32) -> Self {
self.style.padding.right = padding;
self
}
pub fn with_padding_bottom(mut self, padding: f32) -> Self {
self.style.padding.bottom = padding;
self
}
pub fn with_background_color(mut self, color: Color) -> Self {
self.style.background_color = Some(color);
self
}
pub fn with_border(mut self, border: Border) -> Self {
self.style.border = border;
self
}
pub fn with_corner_radius(mut self, radius: f32) -> Self {
self.style.corner_radius = radius;
self
}
pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: Color) -> Self {
self.style.shadow = Some(Shadow {
offset,
blur,
color,
});
self
}
fn margin_size(&self) -> Vector2F {
vec2f(
self.style.margin.left + self.style.margin.right,
self.style.margin.top + self.style.margin.bottom,
)
}
fn padding_size(&self) -> Vector2F {
vec2f(
self.style.padding.left + self.style.padding.right,
self.style.padding.top + self.style.padding.bottom,
)
}
fn border_size(&self) -> Vector2F {
let mut x = 0.0;
if self.style.border.left {
x += self.style.border.width;
}
if self.style.border.right {
x += self.style.border.width;
}
let mut y = 0.0;
if self.style.border.top {
y += self.style.border.width;
}
if self.style.border.bottom {
y += self.style.border.width;
}
vec2f(x, y)
}
}
impl Element for Container {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let mut size_buffer = self.margin_size() + self.padding_size();
if !self.style.border.overlay {
size_buffer += self.border_size();
}
let child_constraint = SizeConstraint {
min: (constraint.min - size_buffer).max(Vector2F::zero()),
max: (constraint.max - size_buffer).max(Vector2F::zero()),
};
let child_size = self.child.layout(child_constraint, cx);
(child_size + size_buffer, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let quad_bounds = RectF::from_points(
bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top),
bounds.lower_right() - vec2f(self.style.margin.right, self.style.margin.bottom),
);
if let Some(shadow) = self.style.shadow.as_ref() {
cx.scene.push_shadow(scene::Shadow {
bounds: quad_bounds + shadow.offset,
corner_radius: self.style.corner_radius,
sigma: shadow.blur,
color: shadow.color,
});
}
let child_origin =
quad_bounds.origin() + vec2f(self.style.padding.left, self.style.padding.top);
if self.style.border.overlay {
cx.scene.push_quad(Quad {
bounds: quad_bounds,
background: self.style.background_color,
border: Default::default(),
corner_radius: self.style.corner_radius,
});
self.child.paint(child_origin, visible_bounds, cx);
cx.scene.push_layer(None);
cx.scene.push_quad(Quad {
bounds: quad_bounds,
background: Default::default(),
border: self.style.border,
corner_radius: self.style.corner_radius,
});
cx.scene.pop_layer();
} else {
cx.scene.push_quad(Quad {
bounds: quad_bounds,
background: self.style.background_color,
border: self.style.border,
corner_radius: self.style.corner_radius,
});
let child_origin = child_origin
+ vec2f(
self.style.border.left_width(),
self.style.border.top_width(),
);
self.child.paint(child_origin, visible_bounds, cx);
}
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &crate::DebugContext,
) -> serde_json::Value {
json!({
"type": "Container",
"bounds": bounds.to_json(),
"details": self.style.to_json(),
"child": self.child.debug(cx),
})
}
}
impl ToJson for ContainerStyle {
fn to_json(&self) -> serde_json::Value {
json!({
"margin": self.margin.to_json(),
"padding": self.padding.to_json(),
"background_color": self.background_color.to_json(),
"border": self.border.to_json(),
"corner_radius": self.corner_radius,
"shadow": self.shadow.to_json(),
})
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Margin {
pub top: f32,
pub left: f32,
pub bottom: f32,
pub right: f32,
}
impl ToJson for Margin {
fn to_json(&self) -> serde_json::Value {
let mut value = json!({});
if self.top > 0. {
value["top"] = json!(self.top);
}
if self.right > 0. {
value["right"] = json!(self.right);
}
if self.bottom > 0. {
value["bottom"] = json!(self.bottom);
}
if self.left > 0. {
value["left"] = json!(self.left);
}
value
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Padding {
pub top: f32,
pub left: f32,
pub bottom: f32,
pub right: f32,
}
impl<'de> Deserialize<'de> for Padding {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let spacing = Spacing::deserialize(deserializer)?;
Ok(match spacing {
Spacing::Uniform(size) => Padding {
top: size,
left: size,
bottom: size,
right: size,
},
Spacing::Specific {
top,
left,
bottom,
right,
} => Padding {
top,
left,
bottom,
right,
},
})
}
}
impl<'de> Deserialize<'de> for Margin {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let spacing = Spacing::deserialize(deserializer)?;
Ok(match spacing {
Spacing::Uniform(size) => Margin {
top: size,
left: size,
bottom: size,
right: size,
},
Spacing::Specific {
top,
left,
bottom,
right,
} => Margin {
top,
left,
bottom,
right,
},
})
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Spacing {
Uniform(f32),
Specific {
#[serde(default)]
top: f32,
#[serde(default)]
left: f32,
#[serde(default)]
bottom: f32,
#[serde(default)]
right: f32,
},
}
impl Padding {
pub fn uniform(padding: f32) -> Self {
Self {
top: padding,
left: padding,
bottom: padding,
right: padding,
}
}
}
impl ToJson for Padding {
fn to_json(&self) -> serde_json::Value {
let mut value = json!({});
if self.top > 0. {
value["top"] = json!(self.top);
}
if self.right > 0. {
value["right"] = json!(self.right);
}
if self.bottom > 0. {
value["bottom"] = json!(self.bottom);
}
if self.left > 0. {
value["left"] = json!(self.left);
}
value
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize)]
pub struct Shadow {
#[serde(default, deserialize_with = "deserialize_vec2f")]
offset: Vector2F,
#[serde(default)]
blur: f32,
#[serde(default)]
color: Color,
}
impl ToJson for Shadow {
fn to_json(&self) -> serde_json::Value {
json!({
"offset": self.offset.to_json(),
"blur": self.blur,
"color": self.color.to_json()
})
}
}

View file

@ -0,0 +1,74 @@
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
DebugContext,
};
use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
pub struct Empty;
impl Empty {
pub fn new() -> Self {
Self
}
}
impl Element for Empty {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
_: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let x = if constraint.max.x().is_finite() {
constraint.max.x()
} else {
constraint.min.x()
};
let y = if constraint.max.y().is_finite() {
constraint.max.y()
} else {
constraint.min.y()
};
(vec2f(x, y), ())
}
fn paint(
&mut self,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut PaintContext,
) -> Self::PaintState {
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "Empty",
"bounds": bounds.to_json(),
})
}
}

View file

@ -0,0 +1,91 @@
use pathfinder_geometry::rect::RectF;
use serde_json::json;
use crate::{
geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext,
LayoutContext, PaintContext, SizeConstraint,
};
pub struct EventHandler {
child: ElementBox,
mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
}
impl EventHandler {
pub fn new(child: ElementBox) -> Self {
Self {
child,
mouse_down: None,
}
}
pub fn on_mouse_down<F>(mut self, callback: F) -> Self
where
F: 'static + FnMut(&mut EventContext) -> bool,
{
self.mouse_down = Some(Box::new(callback));
self
}
}
impl Element for EventHandler {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
if self.child.dispatch_event(event, cx) {
true
} else {
match event {
Event::LeftMouseDown { position, .. } => {
if let Some(callback) = self.mouse_down.as_mut() {
if bounds.contains_point(*position) {
return callback(cx);
}
}
false
}
_ => false,
}
}
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value {
json!({
"type": "EventHandler",
"child": self.child.debug(cx),
})
}
}

View file

@ -0,0 +1,369 @@
use std::{any::Any, f32::INFINITY};
use crate::{
json::{self, ToJson, Value},
Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint, Vector2FExt,
};
use pathfinder_geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
};
use serde_json::json;
pub struct Flex {
axis: Axis,
children: Vec<ElementBox>,
}
impl Flex {
pub fn new(axis: Axis) -> Self {
Self {
axis,
children: Default::default(),
}
}
pub fn row() -> Self {
Self::new(Axis::Horizontal)
}
pub fn column() -> Self {
Self::new(Axis::Vertical)
}
fn layout_flex_children(
&mut self,
expanded: bool,
constraint: SizeConstraint,
remaining_space: &mut f32,
remaining_flex: &mut f32,
cross_axis_max: &mut f32,
cx: &mut LayoutContext,
) {
let cross_axis = self.axis.invert();
for child in &mut self.children {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if metadata.expanded != expanded {
continue;
}
let flex = metadata.flex;
let child_max = if *remaining_flex == 0.0 {
*remaining_space
} else {
let space_per_flex = *remaining_space / *remaining_flex;
space_per_flex * flex
};
let child_min = if expanded { child_max } else { 0. };
let child_constraint = match self.axis {
Axis::Horizontal => SizeConstraint::new(
vec2f(child_min, constraint.min.y()),
vec2f(child_max, constraint.max.y()),
),
Axis::Vertical => SizeConstraint::new(
vec2f(constraint.min.x(), child_min),
vec2f(constraint.max.x(), child_max),
),
};
let child_size = child.layout(child_constraint, cx);
*remaining_space -= child_size.along(self.axis);
*remaining_flex -= flex;
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
}
}
}
}
impl Extend<ElementBox> for Flex {
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
self.children.extend(children);
}
}
impl Element for Flex {
type LayoutState = bool;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let mut total_flex = None;
let mut fixed_space = 0.0;
let cross_axis = self.axis.invert();
let mut cross_axis_max: f32 = 0.0;
for child in &mut self.children {
if let Some(metadata) = child.metadata::<FlexParentData>() {
*total_flex.get_or_insert(0.) += metadata.flex;
} else {
let child_constraint = match self.axis {
Axis::Horizontal => SizeConstraint::new(
vec2f(0.0, constraint.min.y()),
vec2f(INFINITY, constraint.max.y()),
),
Axis::Vertical => SizeConstraint::new(
vec2f(constraint.min.x(), 0.0),
vec2f(constraint.max.x(), INFINITY),
),
};
let size = child.layout(child_constraint, cx);
fixed_space += size.along(self.axis);
cross_axis_max = cross_axis_max.max(size.along(cross_axis));
}
}
let mut size = if let Some(mut remaining_flex) = total_flex {
if constraint.max_along(self.axis).is_infinite() {
panic!("flex contains flexible children but has an infinite constraint along the flex axis");
}
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
self.layout_flex_children(
false,
constraint,
&mut remaining_space,
&mut remaining_flex,
&mut cross_axis_max,
cx,
);
self.layout_flex_children(
true,
constraint,
&mut remaining_space,
&mut remaining_flex,
&mut cross_axis_max,
cx,
);
match self.axis {
Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
}
} else {
match self.axis {
Axis::Horizontal => vec2f(fixed_space, cross_axis_max),
Axis::Vertical => vec2f(cross_axis_max, fixed_space),
}
};
if constraint.min.x().is_finite() {
size.set_x(size.x().max(constraint.min.x()));
}
if constraint.min.y().is_finite() {
size.set_y(size.y().max(constraint.min.y()));
}
let mut overflowing = false;
if size.x() > constraint.max.x() {
size.set_x(constraint.max.x());
overflowing = true;
}
if size.y() > constraint.max.y() {
size.set_y(constraint.max.y());
overflowing = true;
}
(size, overflowing)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
overflowing: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
if *overflowing {
cx.scene.push_layer(Some(bounds));
}
let mut child_origin = bounds.origin();
for child in &mut self.children {
child.paint(child_origin, visible_bounds, cx);
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
}
}
if *overflowing {
cx.scene.pop_layer();
}
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
let mut handled = false;
for child in &mut self.children {
handled = child.dispatch_event(event, cx) || handled;
}
handled
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> json::Value {
json!({
"type": "Flex",
"bounds": bounds.to_json(),
"axis": self.axis.to_json(),
"children": self.children.iter().map(|child| child.debug(cx)).collect::<Vec<json::Value>>()
})
}
}
struct FlexParentData {
flex: f32,
expanded: bool,
}
pub struct Expanded {
metadata: FlexParentData,
child: ElementBox,
}
impl Expanded {
pub fn new(flex: f32, child: ElementBox) -> Self {
Expanded {
metadata: FlexParentData {
flex,
expanded: true,
},
child,
}
}
}
impl Element for Expanded {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx)
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn metadata(&self) -> Option<&dyn Any> {
Some(&self.metadata)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> Value {
json!({
"type": "Expanded",
"flex": self.metadata.flex,
"child": self.child.debug(cx)
})
}
}
pub struct Flexible {
metadata: FlexParentData,
child: ElementBox,
}
impl Flexible {
pub fn new(flex: f32, child: ElementBox) -> Self {
Flexible {
metadata: FlexParentData {
flex,
expanded: false,
},
child,
}
}
}
impl Element for Flexible {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx)
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn metadata(&self) -> Option<&dyn Any> {
Some(&self.metadata)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> Value {
json!({
"type": "Flexible",
"flex": self.metadata.flex,
"child": self.child.debug(cx)
})
}
}

View file

@ -0,0 +1,79 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::json,
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
pub struct Hook {
child: ElementBox,
after_layout: Option<Box<dyn FnMut(Vector2F, &mut LayoutContext)>>,
}
impl Hook {
pub fn new(child: ElementBox) -> Self {
Self {
child,
after_layout: None,
}
}
pub fn on_after_layout(
mut self,
f: impl 'static + FnMut(Vector2F, &mut LayoutContext),
) -> Self {
self.after_layout = Some(Box::new(f));
self
}
}
impl Element for Hook {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
if let Some(handler) = self.after_layout.as_mut() {
handler(size, cx);
}
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value {
json!({
"type": "Hooks",
"child": self.child.debug(cx),
})
}
}

View file

@ -0,0 +1,103 @@
use super::constrain_size_preserving_aspect_ratio;
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext,
PaintContext, SizeConstraint,
};
use serde::Deserialize;
use std::sync::Arc;
pub struct Image {
data: Arc<ImageData>,
style: ImageStyle,
}
#[derive(Copy, Clone, Default, Deserialize)]
pub struct ImageStyle {
#[serde(default)]
pub border: Border,
#[serde(default)]
pub corner_radius: f32,
#[serde(default)]
pub height: Option<f32>,
#[serde(default)]
pub width: Option<f32>,
}
impl Image {
pub fn new(data: Arc<ImageData>) -> Self {
Self {
data,
style: Default::default(),
}
}
pub fn with_style(mut self, style: ImageStyle) -> Self {
self.style = style;
self
}
}
impl Element for Image {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
_: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let desired_size = vec2f(
self.style.width.unwrap_or(constraint.max.x()),
self.style.height.unwrap_or(constraint.max.y()),
);
let size = constrain_size_preserving_aspect_ratio(
constraint.constrain(desired_size),
self.data.size().to_f32(),
);
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
_: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_image(scene::Image {
bounds,
border: self.style.border,
corner_radius: self.style.corner_radius,
data: self.data.clone(),
});
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "Image",
"bounds": bounds.to_json(),
})
}
}

View file

@ -0,0 +1,263 @@
use crate::{
fonts::TextStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{ToJson, Value},
text_layout::{Line, RunStyle},
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use serde::Deserialize;
use serde_json::json;
use smallvec::{smallvec, SmallVec};
pub struct Label {
text: String,
style: LabelStyle,
highlight_indices: Vec<usize>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct LabelStyle {
pub text: TextStyle,
pub highlight_text: Option<TextStyle>,
}
impl From<TextStyle> for LabelStyle {
fn from(text: TextStyle) -> Self {
LabelStyle {
text,
highlight_text: None,
}
}
}
impl Label {
pub fn new(text: String, style: impl Into<LabelStyle>) -> Self {
Self {
text,
highlight_indices: Default::default(),
style: style.into(),
}
}
pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
self.highlight_indices = indices;
self
}
fn compute_runs(&self) -> SmallVec<[(usize, RunStyle); 8]> {
let font_id = self.style.text.font_id;
if self.highlight_indices.is_empty() {
return smallvec![(
self.text.len(),
RunStyle {
font_id,
color: self.style.text.color,
underline: self.style.text.underline,
}
)];
}
let highlight_font_id = self
.style
.highlight_text
.as_ref()
.map_or(font_id, |style| style.font_id);
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
let mut runs = SmallVec::new();
let highlight_style = self
.style
.highlight_text
.as_ref()
.unwrap_or(&self.style.text);
for (char_ix, c) in self.text.char_indices() {
let mut font_id = font_id;
let mut color = self.style.text.color;
let mut underline = self.style.text.underline;
if let Some(highlight_ix) = highlight_indices.peek() {
if char_ix == *highlight_ix {
font_id = highlight_font_id;
color = highlight_style.color;
underline = highlight_style.underline;
highlight_indices.next();
}
}
let last_run: Option<&mut (usize, RunStyle)> = runs.last_mut();
let push_new_run = if let Some((last_len, last_style)) = last_run {
if font_id == last_style.font_id
&& color == last_style.color
&& underline == last_style.underline
{
*last_len += c.len_utf8();
false
} else {
true
}
} else {
true
};
if push_new_run {
runs.push((
c.len_utf8(),
RunStyle {
font_id,
color,
underline,
},
));
}
}
runs
}
}
impl Element for Label {
type LayoutState = Line;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let runs = self.compute_runs();
let line = cx.text_layout_cache.layout_str(
self.text.as_str(),
self.style.text.font_size,
runs.as_slice(),
);
let size = vec2f(
line.width()
.ceil()
.max(constraint.min.x())
.min(constraint.max.x()),
cx.font_cache
.line_height(self.style.text.font_id, self.style.text.font_size),
);
(size, line)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
line: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> Value {
json!({
"type": "Label",
"bounds": bounds.to_json(),
"text": &self.text,
"highlight_indices": self.highlight_indices,
"style": self.style.to_json(),
})
}
}
impl ToJson for LabelStyle {
fn to_json(&self) -> Value {
json!({
"text": self.text.to_json(),
"highlight_text": self.highlight_text
.as_ref()
.map_or(serde_json::Value::Null, |style| style.to_json())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
use crate::fonts::{Properties as FontProperties, Weight};
#[crate::test(self)]
fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) {
let default_style = TextStyle::new(
"Menlo",
12.,
Default::default(),
false,
Color::black(),
cx.font_cache(),
)
.unwrap();
let highlight_style = TextStyle::new(
"Menlo",
12.,
*FontProperties::new().weight(Weight::BOLD),
false,
Color::new(255, 0, 0, 255),
cx.font_cache(),
)
.unwrap();
let label = Label::new(
".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(),
LabelStyle {
text: default_style.clone(),
highlight_text: Some(highlight_style.clone()),
},
)
.with_highlights(vec![
".α".len(),
".αβ".len(),
".αβγδ".len(),
".αβγδε.ⓐ".len(),
".αβγδε.ⓐⓑ".len(),
]);
let default_run_style = RunStyle {
font_id: default_style.font_id,
color: default_style.color,
underline: default_style.underline,
};
let highlight_run_style = RunStyle {
font_id: highlight_style.font_id,
color: highlight_style.color,
underline: highlight_style.underline,
};
let runs = label.compute_runs();
assert_eq!(
runs.as_slice(),
&[
(".α".len(), default_run_style),
("βγ".len(), highlight_run_style),
("δ".len(), default_run_style),
("ε".len(), highlight_run_style),
(".ⓐ".len(), default_run_style),
("ⓑⓒ".len(), highlight_run_style),
("ⓓⓔ.abcde.".len(), default_run_style),
]
);
}
}

View file

@ -0,0 +1,890 @@
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::json,
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
use sum_tree::{self, Bias, SumTree};
pub struct List {
state: ListState,
invalidated_elements: Vec<ElementRc>,
}
#[derive(Clone)]
pub struct ListState(Rc<RefCell<StateInner>>);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Orientation {
Top,
Bottom,
}
struct StateInner {
last_layout_width: Option<f32>,
render_item: Box<dyn FnMut(usize, &mut LayoutContext) -> ElementBox>,
rendered_range: Range<usize>,
items: SumTree<ListItem>,
logical_scroll_top: Option<ListOffset>,
orientation: Orientation,
overdraw: f32,
scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ListOffset {
item_ix: usize,
offset_in_item: f32,
}
#[derive(Clone)]
enum ListItem {
Unrendered,
Rendered(ElementRc),
Removed(f32),
}
impl std::fmt::Debug for ListItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unrendered => write!(f, "Unrendered"),
Self::Rendered(_) => f.debug_tuple("Rendered").finish(),
Self::Removed(height) => f.debug_tuple("Removed").field(height).finish(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
struct ListItemSummary {
count: usize,
rendered_count: usize,
unrendered_count: usize,
height: f32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct Count(usize);
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct RenderedCount(usize);
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct UnrenderedCount(usize);
#[derive(Clone, Debug, Default)]
struct Height(f32);
impl List {
pub fn new(state: ListState) -> Self {
Self {
state,
invalidated_elements: Default::default(),
}
}
}
impl Element for List {
type LayoutState = ListOffset;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let state = &mut *self.state.0.borrow_mut();
let size = constraint.max;
let mut item_constraint = constraint;
item_constraint.min.set_y(0.);
item_constraint.max.set_y(f32::INFINITY);
if cx.refreshing || state.last_layout_width != Some(size.x()) {
state.rendered_range = 0..0;
state.items = SumTree::from_iter(
(0..state.items.summary().count).map(|_| ListItem::Unrendered),
&(),
)
}
let old_items = state.items.clone();
let mut new_items = SumTree::new();
let mut rendered_items = VecDeque::new();
let mut rendered_height = 0.;
let mut scroll_top = state
.logical_scroll_top
.unwrap_or_else(|| match state.orientation {
Orientation::Top => ListOffset {
item_ix: 0,
offset_in_item: 0.,
},
Orientation::Bottom => ListOffset {
item_ix: state.items.summary().count,
offset_in_item: 0.,
},
});
// Render items after the scroll top, including those in the trailing overdraw.
let mut cursor = old_items.cursor::<Count>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
for (ix, item) in cursor.by_ref().enumerate() {
if rendered_height - scroll_top.offset_in_item >= size.y() + state.overdraw {
break;
}
let element = state.render_item(scroll_top.item_ix + ix, item, item_constraint, cx);
rendered_height += element.size().y();
rendered_items.push_back(ListItem::Rendered(element));
}
// Prepare to start walking upward from the item at the scroll top.
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
// If the rendered items do not fill the visible region, then adjust
// the scroll top upward.
if rendered_height - scroll_top.offset_in_item < size.y() {
while rendered_height < size.y() {
cursor.prev(&());
if let Some(item) = cursor.item() {
let element = state.render_item(cursor.start().0, item, item_constraint, cx);
rendered_height += element.size().y();
rendered_items.push_front(ListItem::Rendered(element));
} else {
break;
}
}
scroll_top = ListOffset {
item_ix: cursor.start().0,
offset_in_item: rendered_height - size.y(),
};
match state.orientation {
Orientation::Top => {
scroll_top.offset_in_item = scroll_top.offset_in_item.max(0.);
state.logical_scroll_top = Some(scroll_top);
}
Orientation::Bottom => {
scroll_top = ListOffset {
item_ix: cursor.start().0,
offset_in_item: rendered_height - size.y(),
};
state.logical_scroll_top = None;
}
};
}
// Render items in the leading overdraw.
let mut leading_overdraw = scroll_top.offset_in_item;
while leading_overdraw < state.overdraw {
cursor.prev(&());
if let Some(item) = cursor.item() {
let element = state.render_item(cursor.start().0, item, item_constraint, cx);
leading_overdraw += element.size().y();
rendered_items.push_front(ListItem::Rendered(element));
} else {
break;
}
}
let new_rendered_range = cursor.start().0..(cursor.start().0 + rendered_items.len());
let mut cursor = old_items.cursor::<Count>();
if state.rendered_range.start < new_rendered_range.start {
new_items.push_tree(
cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
&(),
);
let remove_to = state.rendered_range.end.min(new_rendered_range.start);
while cursor.start().0 < remove_to {
new_items.push(cursor.item().unwrap().remove(), &());
cursor.next(&());
}
}
new_items.push_tree(
cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
&(),
);
new_items.extend(rendered_items, &());
cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
if new_rendered_range.end < state.rendered_range.start {
new_items.push_tree(
cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
&(),
);
}
while cursor.start().0 < state.rendered_range.end {
new_items.push(cursor.item().unwrap().remove(), &());
cursor.next(&());
}
new_items.push_tree(cursor.suffix(&()), &());
state.items = new_items;
state.rendered_range = new_rendered_range;
state.last_layout_width = Some(size.x());
(size, scroll_top)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
scroll_top: &mut ListOffset,
cx: &mut PaintContext,
) {
cx.scene.push_layer(Some(bounds));
let state = &mut *self.state.0.borrow_mut();
for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
element.paint(origin, visible_bounds, cx);
}
cx.scene.pop_layer();
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
scroll_top: &mut ListOffset,
_: &mut (),
cx: &mut EventContext,
) -> bool {
let mut handled = false;
let mut state = self.state.0.borrow_mut();
let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
let mut cursor = state.items.cursor::<Count>();
let mut new_items = cursor.slice(&Count(scroll_top.item_ix), Bias::Right, &());
while let Some(item) = cursor.item() {
if item_origin.y() > bounds.max_y() {
break;
}
if let ListItem::Rendered(element) = item {
let prev_notify_count = cx.notify_count();
let mut element = element.clone();
handled = element.dispatch_event(event, cx) || handled;
item_origin.set_y(item_origin.y() + element.size().y());
if cx.notify_count() > prev_notify_count {
new_items.push(ListItem::Unrendered, &());
self.invalidated_elements.push(element);
} else {
new_items.push(item.clone(), &());
}
cursor.next(&());
} else {
unreachable!();
}
}
new_items.push_tree(cursor.suffix(&()), &());
drop(cursor);
state.items = new_items;
match event {
Event::ScrollWheel {
position,
delta,
precise,
} => {
if bounds.contains_point(*position) {
if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
handled = true;
}
}
}
_ => {}
}
handled
}
fn debug(
&self,
bounds: RectF,
scroll_top: &Self::LayoutState,
_: &(),
cx: &DebugContext,
) -> serde_json::Value {
let state = self.state.0.borrow_mut();
let visible_elements = state
.visible_elements(bounds, scroll_top)
.map(|e| e.0.debug(cx))
.collect::<Vec<_>>();
let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
json!({
"visible_range": visible_range,
"visible_elements": visible_elements,
"scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
})
}
}
impl ListState {
pub fn new<F>(
element_count: usize,
orientation: Orientation,
overdraw: f32,
render_item: F,
) -> Self
where
F: 'static + FnMut(usize, &mut LayoutContext) -> ElementBox,
{
let mut items = SumTree::new();
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
Self(Rc::new(RefCell::new(StateInner {
last_layout_width: None,
render_item: Box::new(render_item),
rendered_range: 0..0,
items,
logical_scroll_top: None,
orientation,
overdraw,
scroll_handler: None,
})))
}
pub fn reset(&self, element_count: usize) {
let state = &mut *self.0.borrow_mut();
state.rendered_range = 0..0;
state.logical_scroll_top = None;
state.items = SumTree::new();
state
.items
.extend((0..element_count).map(|_| ListItem::Unrendered), &());
}
pub fn splice(&self, old_range: Range<usize>, count: usize) {
let state = &mut *self.0.borrow_mut();
if let Some(ListOffset {
item_ix,
offset_in_item,
}) = state.logical_scroll_top.as_mut()
{
if old_range.contains(item_ix) {
*item_ix = old_range.start;
*offset_in_item = 0.;
} else if old_range.end <= *item_ix {
*item_ix = *item_ix - (old_range.end - old_range.start) + count;
}
}
let new_end = old_range.start + count;
if old_range.start < state.rendered_range.start {
state.rendered_range.start =
new_end + state.rendered_range.start.saturating_sub(old_range.end);
}
if old_range.start < state.rendered_range.end {
state.rendered_range.end =
new_end + state.rendered_range.end.saturating_sub(old_range.end);
}
let mut old_heights = state.items.cursor::<Count>();
let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
new_heights.push_tree(old_heights.suffix(&()), &());
drop(old_heights);
state.items = new_heights;
}
pub fn set_scroll_handler(
&mut self,
handler: impl FnMut(Range<usize>, &mut EventContext) + 'static,
) {
self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
}
}
impl StateInner {
fn render_item(
&mut self,
ix: usize,
existing_item: &ListItem,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> ElementRc {
if let ListItem::Rendered(element) = existing_item {
element.clone()
} else {
let mut element = (self.render_item)(ix, cx);
element.layout(constraint, cx);
element.into()
}
}
fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
let start_y = cursor.start().height + scroll_top.offset_in_item;
cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
scroll_top.item_ix..cursor.start().count + 1
}
fn visible_elements<'a>(
&'a self,
bounds: RectF,
scroll_top: &ListOffset,
) -> impl Iterator<Item = (ElementRc, Vector2F)> + 'a {
let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
let mut cursor = self.items.cursor::<Count>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
std::iter::from_fn(move || {
while let Some(item) = cursor.item() {
if item_origin.y() > bounds.max_y() {
break;
}
if let ListItem::Rendered(element) = item {
let result = (element.clone(), item_origin);
item_origin.set_y(item_origin.y() + element.size().y());
cursor.next(&());
return Some(result);
}
cursor.next(&());
}
None
})
}
fn scroll(
&mut self,
scroll_top: &ListOffset,
height: f32,
mut delta: Vector2F,
precise: bool,
cx: &mut EventContext,
) -> bool {
if !precise {
delta *= 20.;
}
let scroll_max = (self.items.summary().height - height).max(0.);
let new_scroll_top = (self.scroll_top(scroll_top) - delta.y())
.max(0.)
.min(scroll_max);
if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
self.logical_scroll_top = None;
} else {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
let item_ix = cursor.start().count;
let offset_in_item = new_scroll_top - cursor.start().height;
self.logical_scroll_top = Some(ListOffset {
item_ix,
offset_in_item,
});
}
if self.scroll_handler.is_some() {
let visible_range = self.visible_range(height, scroll_top);
self.scroll_handler.as_mut().unwrap()(visible_range, cx);
}
cx.notify();
true
}
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
cursor.start().height + logical_scroll_top.offset_in_item
}
}
impl ListItem {
fn remove(&self) -> Self {
match self {
ListItem::Unrendered => ListItem::Unrendered,
ListItem::Rendered(element) => ListItem::Removed(element.size().y()),
ListItem::Removed(height) => ListItem::Removed(*height),
}
}
}
impl sum_tree::Item for ListItem {
type Summary = ListItemSummary;
fn summary(&self) -> Self::Summary {
match self {
ListItem::Unrendered => ListItemSummary {
count: 1,
rendered_count: 0,
unrendered_count: 1,
height: 0.,
},
ListItem::Rendered(element) => ListItemSummary {
count: 1,
rendered_count: 1,
unrendered_count: 0,
height: element.size().y(),
},
ListItem::Removed(height) => ListItemSummary {
count: 1,
rendered_count: 0,
unrendered_count: 1,
height: *height,
},
}
}
}
impl sum_tree::Summary for ListItemSummary {
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &()) {
self.count += summary.count;
self.rendered_count += summary.rendered_count;
self.unrendered_count += summary.unrendered_count;
self.height += summary.height;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.rendered_count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.unrendered_count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.height;
}
}
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
self.0.partial_cmp(&other.count).unwrap()
}
}
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
self.0.partial_cmp(&other.height).unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::vector::vec2f;
use rand::prelude::*;
use std::env;
#[crate::test(self)]
fn test_layout(cx: &mut crate::MutableAppContext) {
let mut presenter = cx.build_presenter(0, 0.);
let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
let elements = elements.clone();
move |ix, _| {
let (id, height) = elements.borrow()[ix];
TestElement::new(id, height).boxed()
}
});
let mut list = List::new(state.clone());
let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(false, cx));
assert_eq!(size, vec2f(100., 40.));
assert_eq!(
state.0.borrow().items.summary(),
ListItemSummary {
count: 3,
rendered_count: 3,
unrendered_count: 0,
height: 150.
}
);
state.0.borrow_mut().scroll(
&ListOffset {
item_ix: 0,
offset_in_item: 0.,
},
40.,
vec2f(0., -54.),
true,
&mut presenter.build_event_context(cx),
);
let (_, logical_scroll_top) =
list.layout(constraint, &mut presenter.build_layout_context(false, cx));
assert_eq!(
logical_scroll_top,
ListOffset {
item_ix: 2,
offset_in_item: 4.
}
);
assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
elements.borrow_mut().push((5, 60.));
state.splice(1..2, 2);
state.splice(4..4, 1);
assert_eq!(
state.0.borrow().items.summary(),
ListItemSummary {
count: 5,
rendered_count: 2,
unrendered_count: 3,
height: 120.
}
);
let (size, logical_scroll_top) =
list.layout(constraint, &mut presenter.build_layout_context(false, cx));
assert_eq!(size, vec2f(100., 40.));
assert_eq!(
state.0.borrow().items.summary(),
ListItemSummary {
count: 5,
rendered_count: 5,
unrendered_count: 0,
height: 270.
}
);
assert_eq!(
logical_scroll_top,
ListOffset {
item_ix: 3,
offset_in_item: 4.
}
);
assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
}
#[crate::test(self, iterations = 10, seed = 0)]
fn test_random(cx: &mut crate::MutableAppContext, mut rng: StdRng) {
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let mut presenter = cx.build_presenter(0, 0.);
let mut next_id = 0;
let elements = Rc::new(RefCell::new(
(0..rng.gen_range(0..=20))
.map(|_| {
let id = next_id;
next_id += 1;
(id, rng.gen_range(0..=200) as f32 / 2.0)
})
.collect::<Vec<_>>(),
));
let orientation = *[Orientation::Top, Orientation::Bottom]
.choose(&mut rng)
.unwrap();
let overdraw = rng.gen_range(1..=100) as f32;
let state = ListState::new(elements.borrow().len(), orientation, overdraw, {
let elements = elements.clone();
move |ix, _| {
let (id, height) = elements.borrow()[ix];
TestElement::new(id, height).boxed()
}
});
let mut width = rng.gen_range(0..=2000) as f32 / 2.;
let mut height = rng.gen_range(0..=2000) as f32 / 2.;
log::info!("orientation: {:?}", orientation);
log::info!("overdraw: {}", overdraw);
log::info!("elements: {:?}", elements.borrow());
log::info!("size: ({:?}, {:?})", width, height);
log::info!("==================");
let mut last_logical_scroll_top = None;
for _ in 0..operations {
match rng.gen_range(0..=100) {
0..=29 if last_logical_scroll_top.is_some() => {
let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
log::info!(
"Scrolling by {:?}, previous scroll top: {:?}",
delta,
last_logical_scroll_top.unwrap()
);
state.0.borrow_mut().scroll(
last_logical_scroll_top.as_ref().unwrap(),
height,
delta,
true,
&mut presenter.build_event_context(cx),
);
}
30..=34 => {
width = rng.gen_range(0..=2000) as f32 / 2.;
log::info!("changing width: {:?}", width);
}
35..=54 => {
height = rng.gen_range(0..=1000) as f32 / 2.;
log::info!("changing height: {:?}", height);
}
_ => {
let mut elements = elements.borrow_mut();
let end_ix = rng.gen_range(0..=elements.len());
let start_ix = rng.gen_range(0..=end_ix);
let new_elements = (0..rng.gen_range(0..10))
.map(|_| {
let id = next_id;
next_id += 1;
(id, rng.gen_range(0..=200) as f32 / 2.)
})
.collect::<Vec<_>>();
log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
state.splice(start_ix..end_ix, new_elements.len());
elements.splice(start_ix..end_ix, new_elements);
for (ix, item) in state.0.borrow().items.cursor::<()>().enumerate() {
if let ListItem::Rendered(element) = item {
let (expected_id, _) = elements[ix];
element.with_metadata(|metadata: Option<&usize>| {
assert_eq!(*metadata.unwrap(), expected_id);
});
}
}
}
}
let mut list = List::new(state.clone());
let (size, logical_scroll_top) = list.layout(
SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)),
&mut presenter.build_layout_context(false, cx),
);
assert_eq!(size, vec2f(width, height));
last_logical_scroll_top = Some(logical_scroll_top);
let state = state.0.borrow();
log::info!("items {:?}", state.items.items(&()));
let scroll_top = state.scroll_top(&logical_scroll_top);
let rendered_top = (scroll_top - overdraw).max(0.);
let rendered_bottom = scroll_top + height + overdraw;
let mut item_top = 0.;
log::info!(
"rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
rendered_top,
rendered_bottom,
scroll_top,
);
let mut first_rendered_element_top = None;
let mut last_rendered_element_bottom = None;
assert_eq!(state.items.summary().count, elements.borrow().len());
for (ix, item) in state.items.cursor::<()>().enumerate() {
match item {
ListItem::Unrendered => {
let item_bottom = item_top;
assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
item_top = item_bottom;
}
ListItem::Removed(height) => {
let (id, expected_height) = elements.borrow()[ix];
assert_eq!(
*height, expected_height,
"element {} height didn't match",
id
);
let item_bottom = item_top + height;
assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
item_top = item_bottom;
}
ListItem::Rendered(element) => {
let (expected_id, expected_height) = elements.borrow()[ix];
element.with_metadata(|metadata: Option<&usize>| {
assert_eq!(*metadata.unwrap(), expected_id);
});
assert_eq!(element.size().y(), expected_height);
let item_bottom = item_top + element.size().y();
first_rendered_element_top.get_or_insert(item_top);
last_rendered_element_bottom = Some(item_bottom);
assert!(item_bottom > rendered_top || item_top < rendered_bottom);
item_top = item_bottom;
}
}
}
match orientation {
Orientation::Top => {
if let Some(first_rendered_element_top) = first_rendered_element_top {
assert!(first_rendered_element_top <= scroll_top);
}
}
Orientation::Bottom => {
if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
assert!(last_rendered_element_bottom >= scroll_top + height);
}
}
}
}
}
struct TestElement {
id: usize,
size: Vector2F,
}
impl TestElement {
fn new(id: usize, height: f32) -> Self {
Self {
id,
size: vec2f(100., height),
}
}
}
impl Element for TestElement {
type LayoutState = ();
type PaintState = ();
fn layout(&mut self, _: SizeConstraint, _: &mut LayoutContext) -> (Vector2F, ()) {
(self.size, ())
}
fn paint(&mut self, _: RectF, _: RectF, _: &mut (), _: &mut PaintContext) {
todo!()
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut (),
_: &mut (),
_: &mut EventContext,
) -> bool {
todo!()
}
fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value {
self.id.into()
}
fn metadata(&self) -> Option<&dyn std::any::Any> {
Some(&self.id)
}
}
}

View file

@ -0,0 +1,208 @@
use super::Padding;
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
platform::CursorStyle,
CursorStyleHandle, DebugContext, Element, ElementBox, ElementStateHandle, ElementStateId,
Event, EventContext, LayoutContext, MutableAppContext, PaintContext, SizeConstraint,
};
use serde_json::json;
use std::ops::DerefMut;
pub struct MouseEventHandler {
state: ElementStateHandle<MouseState>,
child: ElementBox,
cursor_style: Option<CursorStyle>,
mouse_down_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
click_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
drag_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
padding: Padding,
}
#[derive(Default)]
pub struct MouseState {
pub hovered: bool,
pub clicked: bool,
prev_drag_position: Option<Vector2F>,
cursor_style_handle: Option<CursorStyleHandle>,
}
impl MouseEventHandler {
pub fn new<Tag, F, C, Id>(id: Id, cx: &mut C, render_child: F) -> Self
where
Tag: 'static,
F: FnOnce(&MouseState, &mut C) -> ElementBox,
C: DerefMut<Target = MutableAppContext>,
Id: Into<ElementStateId>,
{
let state_handle = cx.element_state::<Tag, _>(id.into());
let child = state_handle.update(cx, |state, cx| render_child(state, cx));
Self {
state: state_handle,
child,
cursor_style: None,
mouse_down_handler: None,
click_handler: None,
drag_handler: None,
padding: Default::default(),
}
}
pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
self.cursor_style = Some(cursor);
self
}
pub fn on_mouse_down(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
self.mouse_down_handler = Some(Box::new(handler));
self
}
pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
self.click_handler = Some(Box::new(handler));
self
}
pub fn on_drag(mut self, handler: impl FnMut(Vector2F, &mut EventContext) + 'static) -> Self {
self.drag_handler = Some(Box::new(handler));
self
}
pub fn with_padding(mut self, padding: Padding) -> Self {
self.padding = padding;
self
}
}
impl Element for MouseEventHandler {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, cx), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
let cursor_style = self.cursor_style;
let mouse_down_handler = self.mouse_down_handler.as_mut();
let click_handler = self.click_handler.as_mut();
let drag_handler = self.drag_handler.as_mut();
let handled_in_child = self.child.dispatch_event(event, cx);
let hit_bounds = RectF::from_points(
bounds.origin() - vec2f(self.padding.left, self.padding.top),
bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
)
.round_out();
self.state.update(cx, |state, cx| match event {
Event::MouseMoved {
position,
left_mouse_down,
} => {
if !left_mouse_down {
let mouse_in = hit_bounds.contains_point(*position);
if state.hovered != mouse_in {
state.hovered = mouse_in;
if let Some(cursor_style) = cursor_style {
if !state.clicked {
if state.hovered {
state.cursor_style_handle =
Some(cx.set_cursor_style(cursor_style));
} else {
state.cursor_style_handle = None;
}
}
}
cx.notify();
return true;
}
}
handled_in_child
}
Event::LeftMouseDown { position, .. } => {
if !handled_in_child && hit_bounds.contains_point(*position) {
state.clicked = true;
state.prev_drag_position = Some(*position);
cx.notify();
if let Some(handler) = mouse_down_handler {
handler(cx);
}
true
} else {
handled_in_child
}
}
Event::LeftMouseUp { position, .. } => {
state.prev_drag_position = None;
if !handled_in_child && state.clicked {
state.clicked = false;
if !state.hovered {
state.cursor_style_handle = None;
}
cx.notify();
if let Some(handler) = click_handler {
if hit_bounds.contains_point(*position) {
handler(cx);
}
}
true
} else {
handled_in_child
}
}
Event::LeftMouseDragged { position, .. } => {
if !handled_in_child && state.clicked {
let prev_drag_position = state.prev_drag_position.replace(*position);
if let Some((handler, prev_position)) = drag_handler.zip(prev_drag_position) {
let delta = *position - prev_position;
if !delta.is_zero() {
(handler)(delta, cx);
}
}
true
} else {
handled_in_child
}
}
_ => handled_in_child,
})
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value {
json!({
"type": "MouseEventHandler",
"child": self.child.debug(cx),
})
}
}

View file

@ -0,0 +1,63 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
pub struct Overlay {
child: ElementBox,
}
impl Overlay {
pub fn new(child: ElementBox) -> Self {
Self { child }
}
}
impl Element for Overlay {
type LayoutState = Vector2F;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, cx);
(Vector2F::zero(), size)
}
fn paint(
&mut self,
bounds: RectF,
_: RectF,
size: &mut Self::LayoutState,
cx: &mut PaintContext,
) {
let bounds = RectF::new(bounds.origin(), *size);
cx.scene.push_stacking_context(None);
self.child.paint(bounds.origin(), bounds, cx);
cx.scene.pop_stacking_context();
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> serde_json::Value {
self.child.debug(cx)
}
}

View file

@ -0,0 +1,85 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::{self, json, ToJson},
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
};
pub struct Stack {
children: Vec<ElementBox>,
}
impl Stack {
pub fn new() -> Self {
Stack {
children: Vec::new(),
}
}
}
impl Element for Stack {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.min;
for child in &mut self.children {
size = size.max(child.layout(constraint, cx));
}
(size, ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
for child in &mut self.children {
cx.scene.push_layer(None);
child.paint(bounds.origin(), visible_bounds, cx);
cx.scene.pop_layer();
}
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
for child in self.children.iter_mut().rev() {
if child.dispatch_event(event, cx) {
return true;
}
}
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> json::Value {
json!({
"type": "Stack",
"bounds": bounds.to_json(),
"children": self.children.iter().map(|child| child.debug(cx)).collect::<Vec<json::Value>>()
})
}
}
impl Extend<ElementBox> for Stack {
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
self.children.extend(children)
}
}

View file

@ -0,0 +1,111 @@
use std::borrow::Cow;
use serde_json::json;
use crate::{
color::Color,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
scene, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
pub struct Svg {
path: Cow<'static, str>,
color: Color,
}
impl Svg {
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
Self {
path: path.into(),
color: Color::black(),
}
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
}
impl Element for Svg {
type LayoutState = Option<usvg::Tree>;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
match cx.asset_cache.svg(&self.path) {
Ok(tree) => {
let size = constrain_size_preserving_aspect_ratio(
constraint.max,
from_usvg_rect(tree.svg_node().view_box.rect).size(),
);
(size, Some(tree))
}
Err(_error) => {
#[cfg(not(any(test, feature = "test-support")))]
log::error!("{}", _error);
(constraint.min, None)
}
}
}
fn paint(
&mut self,
bounds: RectF,
_visible_bounds: RectF,
svg: &mut Self::LayoutState,
cx: &mut PaintContext,
) {
if let Some(svg) = svg.clone() {
cx.scene.push_icon(scene::Icon {
bounds,
svg,
path: self.path.clone(),
color: self.color,
});
}
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "Svg",
"bounds": bounds.to_json(),
"path": self.path,
"color": self.color.to_json(),
})
}
}
use crate::json::ToJson;
use super::constrain_size_preserving_aspect_ratio;
fn from_usvg_rect(rect: usvg::Rect) -> RectF {
RectF::new(
vec2f(rect.x() as f32, rect.y() as f32),
vec2f(rect.width() as f32, rect.height() as f32),
)
}

View file

@ -0,0 +1,131 @@
use crate::{
color::Color,
fonts::TextStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{ToJson, Value},
text_layout::{Line, ShapedBoundary},
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use serde_json::json;
pub struct Text {
text: String,
style: TextStyle,
}
pub struct LayoutState {
lines: Vec<(Line, Vec<ShapedBoundary>)>,
line_height: f32,
}
impl Text {
pub fn new(text: String, style: TextStyle) -> Self {
Self { text, style }
}
pub fn with_default_color(mut self, color: Color) -> Self {
self.style.color = color;
self
}
}
impl Element for Text {
type LayoutState = LayoutState;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let font_id = self.style.font_id;
let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
let mut lines = Vec::new();
let mut line_count = 0;
let mut max_line_width = 0_f32;
for line in self.text.lines() {
let shaped_line = cx.text_layout_cache.layout_str(
line,
self.style.font_size,
&[(line.len(), self.style.to_run())],
);
let wrap_boundaries = wrapper
.wrap_shaped_line(line, &shaped_line, constraint.max.x())
.collect::<Vec<_>>();
max_line_width = max_line_width.max(shaped_line.width());
line_count += wrap_boundaries.len() + 1;
lines.push((shaped_line, wrap_boundaries));
}
let size = vec2f(
max_line_width
.ceil()
.max(constraint.min.x())
.min(constraint.max.x()),
(line_height * line_count as f32).ceil(),
);
(size, LayoutState { lines, line_height })
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let mut origin = bounds.origin();
for (line, wrap_boundaries) in &layout.lines {
let wrapped_line_boundaries = RectF::new(
origin,
vec2f(
bounds.width(),
(wrap_boundaries.len() + 1) as f32 * layout.line_height,
),
);
if wrapped_line_boundaries.intersects(visible_bounds) {
line.paint_wrapped(
origin,
visible_bounds,
layout.line_height,
wrap_boundaries.iter().copied(),
cx,
);
}
origin.set_y(wrapped_line_boundaries.max_y());
}
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> Value {
json!({
"type": "Text",
"bounds": bounds.to_json(),
"text": &self.text,
"style": self.style.to_json(),
})
}
}

View file

@ -0,0 +1,256 @@
use super::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
use crate::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{self, json},
ElementBox,
};
use json::ToJson;
use parking_lot::Mutex;
use std::{cmp, ops::Range, sync::Arc};
#[derive(Clone, Default)]
pub struct UniformListState(Arc<Mutex<StateInner>>);
impl UniformListState {
pub fn scroll_to(&self, item_ix: usize) {
self.0.lock().scroll_to = Some(item_ix);
}
pub fn scroll_top(&self) -> f32 {
self.0.lock().scroll_top
}
}
#[derive(Default)]
struct StateInner {
scroll_top: f32,
scroll_to: Option<usize>,
}
pub struct LayoutState {
scroll_max: f32,
item_height: f32,
items: Vec<ElementBox>,
}
pub struct UniformList<F>
where
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
{
state: UniformListState,
item_count: usize,
append_items: F,
padding_top: f32,
padding_bottom: f32,
}
impl<F> UniformList<F>
where
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
{
pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self {
Self {
state,
item_count,
append_items,
padding_top: 0.,
padding_bottom: 0.,
}
}
pub fn with_padding_top(mut self, padding: f32) -> Self {
self.padding_top = padding;
self
}
pub fn with_padding_bottom(mut self, padding: f32) -> Self {
self.padding_bottom = padding;
self
}
fn scroll(
&self,
_: Vector2F,
mut delta: Vector2F,
precise: bool,
scroll_max: f32,
cx: &mut EventContext,
) -> bool {
if !precise {
delta *= 20.;
}
let mut state = self.state.0.lock();
state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
cx.notify();
true
}
fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
let mut state = self.state.0.lock();
if state.scroll_top > scroll_max {
state.scroll_top = scroll_max;
}
if let Some(item_ix) = state.scroll_to.take() {
let item_top = self.padding_top + item_ix as f32 * item_height;
let item_bottom = item_top + item_height;
if item_top < state.scroll_top {
state.scroll_top = item_top;
} else if item_bottom > (state.scroll_top + list_height) {
state.scroll_top = item_bottom - list_height;
}
}
}
fn scroll_top(&self) -> f32 {
self.state.0.lock().scroll_top
}
}
impl<F> Element for UniformList<F>
where
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
{
type LayoutState = LayoutState;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
if constraint.max.y().is_infinite() {
unimplemented!(
"UniformList does not support being rendered with an unconstrained height"
);
}
let mut size = constraint.max;
let mut item_constraint =
SizeConstraint::new(vec2f(size.x(), 0.0), vec2f(size.x(), f32::INFINITY));
let mut item_height = 0.;
let mut scroll_max = 0.;
let mut items = Vec::new();
(self.append_items)(0..1, &mut items, cx);
if let Some(first_item) = items.first_mut() {
let mut item_size = first_item.layout(item_constraint, cx);
item_size.set_x(size.x());
item_constraint.min = item_size;
item_constraint.max = item_size;
item_height = item_size.y();
let scroll_height = self.item_count as f32 * item_height;
if scroll_height < size.y() {
size.set_y(size.y().min(scroll_height).max(constraint.min.y()));
}
let scroll_height =
item_height * self.item_count as f32 + self.padding_top + self.padding_bottom;
scroll_max = (scroll_height - size.y()).max(0.);
self.autoscroll(scroll_max, size.y(), item_height);
items.clear();
let start = cmp::min(
((self.scroll_top() - self.padding_top) / item_height) as usize,
self.item_count,
);
let end = cmp::min(
self.item_count,
start + (size.y() / item_height).ceil() as usize + 1,
);
(self.append_items)(start..end, &mut items, cx);
for item in &mut items {
item.layout(item_constraint, cx);
}
} else {
size = constraint.min;
}
(
size,
LayoutState {
item_height,
scroll_max,
items,
},
)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_layer(Some(bounds));
let mut item_origin = bounds.origin()
- vec2f(
0.,
(self.state.scroll_top() - self.padding_top) % layout.item_height,
);
for item in &mut layout.items {
item.paint(item_origin, visible_bounds, cx);
item_origin += vec2f(0.0, layout.item_height);
}
cx.scene.pop_layer();
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
layout: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
let mut handled = false;
for item in &mut layout.items {
handled = item.dispatch_event(event, cx) || handled;
}
match event {
Event::ScrollWheel {
position,
delta,
precise,
} => {
if bounds.contains_point(*position) {
if self.scroll(*position, *delta, *precise, layout.scroll_max, cx) {
handled = true;
}
}
}
_ => {}
}
handled
}
fn debug(
&self,
bounds: RectF,
layout: &Self::LayoutState,
_: &Self::PaintState,
cx: &crate::DebugContext,
) -> json::Value {
json!({
"type": "UniformList",
"bounds": bounds.to_json(),
"scroll_max": layout.scroll_max,
"item_height": layout.item_height,
"items": layout.items.iter().map(|item| item.debug(cx)).collect::<Vec<json::Value>>()
})
}
}