use std::collections::HashMap;
use taffy::prelude::*;
use taffy::geometry::Point;
#[derive(Clone, Copy, Debug)]
pub struct Rgba {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Rgba {
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Sides {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl Sides {
pub const fn uniform(v: f32) -> Self {
Self {
top: v,
right: v,
bottom: v,
left: v,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Len {
Px(f32),
Pct(f32),
Vw(f32),
Vh(f32),
}
#[derive(Clone, Copy, Debug)]
pub enum Track {
Px(f32),
Fr(f32),
Auto,
MinMax(TrackSide, TrackSide),
}
#[derive(Clone, Copy, Debug)]
pub enum TrackSide {
Px(f32),
Fr(f32),
Auto,
}
#[derive(Clone, Copy, Debug, Default)]
pub enum Axis {
#[default]
Row,
Column,
}
#[derive(Clone, Copy, Debug)]
pub enum Justify {
Start,
Center,
End,
SpaceBetween,
SpaceAround,
}
#[derive(Clone, Copy, Debug)]
pub enum Align {
Start,
Center,
End,
Stretch,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum TextAlign {
#[default]
Start,
Center,
End,
Justify,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum TextWrap {
#[default]
Normal,
BreakWord,
Anywhere,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Display {
#[default]
Block,
Inline,
Flex,
Grid,
None,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Overflow {
#[default]
Visible,
Clip,
Scroll,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Cursor {
#[default]
Default,
Pointer,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Position {
#[default]
Relative,
Absolute,
}
pub type Corners = [f32; 4];
pub type Transform = [f32; 6];
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum GridFlow {
#[default]
Row,
Column,
RowDense,
ColumnDense,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum GridPlace {
#[default]
Auto,
Line(i16),
Span(u16),
}
#[derive(Clone, Debug)]
pub enum Background {
Color(Rgba),
Gradient(Gradient),
Image(String),
}
#[derive(Clone, Debug)]
pub struct Gradient {
pub kind: GradientKind,
pub stops: Vec<(Rgba, f32)>,
}
#[derive(Clone, Copy, Debug)]
pub enum GradientKind {
Linear { angle: f32 },
Radial,
}
#[derive(Clone, Copy, Debug)]
pub struct BoxShadow {
pub dx: f32,
pub dy: f32,
pub blur: f32,
pub spread: f32,
pub color: Rgba,
pub inset: bool,
}
#[derive(Clone, Debug)]
pub struct Style {
pub display: Display,
pub width: Option<Len>,
pub height: Option<Len>,
pub min_width: Option<Len>,
pub max_width: Option<Len>,
pub min_height: Option<Len>,
pub max_height: Option<Len>,
pub grid_columns: Vec<Track>,
pub grid_rows: Vec<Track>,
pub grid_column: (GridPlace, GridPlace),
pub grid_row: (GridPlace, GridPlace),
pub grid_auto_flow: GridFlow,
pub grid_auto_rows: Vec<Track>,
pub grid_auto_columns: Vec<Track>,
pub grow: f32,
pub shrink: f32,
pub basis: Option<Len>,
pub wrap: bool,
pub opacity: f32,
pub text_wrap: TextWrap,
pub padding: Sides,
pub margin: Sides,
pub border: Sides,
pub border_color: Option<Rgba>,
pub gap: f32,
pub row_gap: Option<f32>,
pub column_gap: Option<f32>,
pub axis: Axis,
pub justify: Option<Justify>,
pub align: Option<Align>,
pub align_self: Option<Align>,
pub justify_self: Option<Align>,
pub justify_items: Option<Align>,
pub align_content: Option<Justify>,
pub overflow: Overflow,
pub background: Option<Background>,
pub radius: Corners,
pub box_shadow: Option<BoxShadow>,
pub transform: Option<Transform>,
pub cursor: Cursor,
pub position: Position,
pub inset: [Option<Len>; 4],
pub aspect_ratio: Option<f32>,
}
impl Default for Style {
fn default() -> Self {
Self {
display: Display::Block,
width: None,
height: None,
min_width: None,
max_width: None,
min_height: None,
max_height: None,
grid_columns: Vec::new(),
grid_rows: Vec::new(),
grid_column: (GridPlace::Auto, GridPlace::Auto),
grid_row: (GridPlace::Auto, GridPlace::Auto),
grid_auto_flow: GridFlow::Row,
grid_auto_rows: Vec::new(),
grid_auto_columns: Vec::new(),
grow: 0.0,
shrink: 1.0,
basis: None,
wrap: false,
opacity: 1.0,
text_wrap: TextWrap::Normal,
padding: Sides::default(),
margin: Sides::default(),
border: Sides::default(),
border_color: None,
gap: 0.0,
row_gap: None,
column_gap: None,
axis: Axis::Row,
justify: None,
align: None,
align_self: None,
justify_self: None,
justify_items: None,
align_content: None,
overflow: Overflow::Visible,
background: None,
radius: [0.0; 4],
box_shadow: None,
transform: None,
cursor: Cursor::Default,
position: Position::Relative,
inset: [None; 4],
aspect_ratio: None,
}
}
}
#[derive(Clone, Debug)]
pub struct ImageContent {
pub src: String,
pub intrinsic: (f32, f32),
}
#[derive(Clone, Debug)]
pub struct TextContent {
pub text: String,
pub font_size: f32,
pub weight: u16,
pub color: Rgba,
pub align: TextAlign,
pub wrap: TextWrap,
pub font_family: Option<String>,
pub letter_spacing: Option<f32>,
pub word_spacing: Option<f32>,
pub line_height: Option<f32>,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub nowrap: bool,
pub caret: Option<usize>,
pub selection: Option<(usize, usize)>,
pub preedit: Option<(usize, usize)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum AccessRole {
#[default]
None,
Label,
Heading,
Button,
CheckBox,
RadioButton,
TextInput,
MultilineTextInput,
ComboBox,
Image,
Link,
ScrollView,
Group,
}
impl AccessRole {
pub fn is_meaningful(self) -> bool {
self != Self::None
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Access {
pub role: AccessRole,
pub label: Option<String>,
pub placeholder: Option<String>,
pub value: Option<String>,
pub checked: Option<bool>,
}
impl Access {
pub fn name(&self) -> Option<&str> {
self.label.as_deref().or(self.placeholder.as_deref())
}
}
#[derive(Clone, Debug)]
pub struct Node {
pub style: Style,
pub text: Option<TextContent>,
pub image: Option<ImageContent>,
pub tick: Option<Rgba>,
pub children: Vec<Node>,
pub on_tap: Option<String>,
pub model: Option<String>,
pub multiline: bool,
pub options: Option<Vec<String>>,
pub hidden: bool,
pub id: Option<String>,
pub label_for: Option<String>,
pub focus_model: Option<String>,
pub state_path: Option<Vec<usize>>,
pub access: Access,
pub instance: Option<String>,
pub key: Option<String>,
}
impl Node {
pub fn new(style: Style) -> Self {
Self {
style,
text: None,
image: None,
tick: None,
children: Vec::new(),
on_tap: None,
model: None,
multiline: false,
options: None,
hidden: false,
id: None,
label_for: None,
focus_model: None,
state_path: None,
access: Access::default(),
instance: None,
key: None,
}
}
pub fn text(style: Style, text: TextContent) -> Self {
Self {
style,
text: Some(text),
image: None,
tick: None,
children: Vec::new(),
on_tap: None,
model: None,
multiline: false,
options: None,
hidden: false,
id: None,
label_for: None,
focus_model: None,
state_path: None,
access: Access::default(),
instance: None,
key: None,
}
}
pub fn image(style: Style, image: ImageContent) -> Self {
Self {
style,
text: None,
image: Some(image),
tick: None,
children: Vec::new(),
on_tap: None,
model: None,
multiline: false,
options: None,
hidden: false,
id: None,
label_for: None,
focus_model: None,
state_path: None,
access: Access::default(),
instance: None,
key: None,
}
}
pub fn with(mut self, child: Node) -> Self {
self.children.push(child);
self
}
}
#[derive(Clone, Debug)]
pub struct PaintRect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub background: Option<Background>,
pub radius: Corners,
pub border_width: f32,
pub border_color: Option<Rgba>,
}
#[derive(Clone, Debug)]
pub struct PaintText {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub content: TextContent,
}
#[derive(Clone, Copy, Debug)]
pub struct PaintTick {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub color: Rgba,
}
#[derive(Clone, Debug)]
pub struct PaintImage {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub content: ImageContent,
}
#[derive(Clone, Debug)]
pub enum Paint {
Rect(PaintRect),
Text(PaintText),
Image(PaintImage),
Tick(PaintTick),
Shadow {
x: f32,
y: f32,
width: f32,
height: f32,
radius: f32,
blur: f32,
color: Rgba,
},
PushClip {
x: f32,
y: f32,
width: f32,
height: f32,
radius: Corners,
},
PopClip,
PushTransform(Transform),
PopTransform,
PushOpacity {
alpha: f32,
width: f32,
height: f32,
},
PopOpacity,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Offset {
pub x: f32,
pub y: f32,
}
impl Offset {
pub fn clamp_to(self, max: Offset) -> Offset {
Offset {
x: self.x.clamp(0.0, max.x),
y: self.y.clamp(0.0, max.y),
}
}
}
#[derive(Clone, Debug)]
pub struct ScrollRegion {
pub id: usize,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub content_width: f32,
pub content_height: f32,
pub max: Offset,
}
impl ScrollRegion {
pub fn contains(&self, px: f32, py: f32) -> bool {
px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
}
pub fn scrollable(&self) -> bool {
self.max.x > 0.0 || self.max.y > 0.0
}
}
#[derive(Clone, Debug)]
pub struct HitRegion {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub on_tap: String,
pub cursor: Cursor,
pub instance: Option<String>,
}
impl HitRegion {
pub fn contains(&self, px: f32, py: f32) -> bool {
px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
}
}
#[derive(Clone, Debug)]
pub struct FocusRegion {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub model: String,
pub row: Option<String>,
pub text: Option<PaintText>,
pub multiline: bool,
pub scroll_id: Option<usize>,
}
impl FocusRegion {
pub fn contains(&self, px: f32, py: f32) -> bool {
px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
}
}
#[derive(Clone, Debug)]
pub struct SelectRegion {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub model: String,
pub row: Option<String>,
pub options: Vec<String>,
}
impl SelectRegion {
pub fn contains(&self, px: f32, py: f32) -> bool {
px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
}
}
#[derive(Clone, Debug)]
pub struct StateRegion {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub path: Vec<usize>,
}
impl StateRegion {
pub fn contains(&self, px: f32, py: f32) -> bool {
px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
}
}
#[derive(Clone, Debug)]
pub struct AccessNode {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub access: Access,
pub model: Option<String>,
}
#[derive(Clone, Debug)]
pub struct FocusItem {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub kind: FocusKind,
pub scroll: Option<usize>,
}
impl FocusItem {
pub fn contains(&self, px: f32, py: f32) -> bool {
px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
}
}
#[derive(Clone, Debug)]
pub enum FocusKind {
Text { model: String, row: Option<String>, multiline: bool, text: Option<PaintText> },
Activate { on_tap: String, instance: Option<String> },
Select { model: String, row: Option<String>, options: Vec<String> },
}
#[derive(Clone, Debug, Default)]
pub struct Layout {
pub paints: Vec<Paint>,
pub hits: Vec<HitRegion>,
pub focuses: Vec<FocusRegion>,
pub selects: Vec<SelectRegion>,
pub focusables: Vec<FocusItem>,
pub scrolls: Vec<ScrollRegion>,
pub states: Vec<StateRegion>,
pub access: Vec<AccessNode>,
}
fn content_box(layout: &taffy::Layout) -> (f32, f32, f32, f32) {
let (p, b) = (layout.padding, layout.border);
(
p.left + b.left,
p.top + b.top,
(layout.size.width - p.left - p.right - b.left - b.right).max(0.0),
(layout.size.height - p.top - p.bottom - b.top - b.bottom).max(0.0),
)
}
pub type Measure<'a> = dyn FnMut(&TextContent, Option<f32>) -> (f32, f32) + 'a;
enum PaintKind {
Box {
bg: Option<Background>,
radius: Corners,
border_width: f32,
border_color: Option<Rgba>,
clip: bool,
shadow: Option<BoxShadow>,
},
Text(TextContent),
Image(ImageContent),
Tick(Rgba),
}
fn to_dim(l: Len, vp: (f32, f32)) -> Dimension {
match l {
Len::Px(v) => length(v),
Len::Pct(p) => percent(p),
Len::Vw(v) => length(vp.0 * v / 100.0),
Len::Vh(v) => length(vp.1 * v / 100.0),
}
}
fn to_placement(p: GridPlace) -> GridPlacement {
match p {
GridPlace::Auto => auto(),
GridPlace::Line(i) => line(i),
GridPlace::Span(n) => span(n),
}
}
fn to_track(t: Track) -> TrackSizingFunction {
match t {
Track::Px(v) => length(v),
Track::Fr(f) => fr(f),
Track::Auto => auto(),
Track::MinMax(lo, hi) => minmax(
match lo {
TrackSide::Px(v) => length(v),
TrackSide::Fr(_) | TrackSide::Auto => auto(),
},
match hi {
TrackSide::Px(v) => length(v),
TrackSide::Fr(f) => fr(f),
TrackSide::Auto => auto(),
},
),
}
}
fn to_auto_track(t: Track) -> taffy::NonRepeatedTrackSizingFunction {
match t {
Track::Px(v) => length(v),
Track::Fr(f) => fr(f),
Track::Auto => auto(),
Track::MinMax(lo, hi) => minmax(
match lo {
TrackSide::Px(v) => length(v),
TrackSide::Fr(_) | TrackSide::Auto => auto(),
},
match hi {
TrackSide::Px(v) => length(v),
TrackSide::Fr(f) => fr(f),
TrackSide::Auto => auto(),
},
),
}
}
fn to_taffy(style: &Style, vp: (f32, f32)) -> taffy::Style {
taffy::Style {
display: match style.display {
Display::Block | Display::Inline => taffy::Display::Block,
Display::Flex => taffy::Display::Flex,
Display::Grid => taffy::Display::Grid,
Display::None => taffy::Display::None,
},
grid_template_columns: style.grid_columns.iter().copied().map(to_track).collect(),
grid_template_rows: style.grid_rows.iter().copied().map(to_track).collect(),
grid_column: Line {
start: to_placement(style.grid_column.0),
end: to_placement(style.grid_column.1),
},
grid_row: Line {
start: to_placement(style.grid_row.0),
end: to_placement(style.grid_row.1),
},
grid_auto_flow: match style.grid_auto_flow {
GridFlow::Row => taffy::GridAutoFlow::Row,
GridFlow::Column => taffy::GridAutoFlow::Column,
GridFlow::RowDense => taffy::GridAutoFlow::RowDense,
GridFlow::ColumnDense => taffy::GridAutoFlow::ColumnDense,
},
grid_auto_rows: style.grid_auto_rows.iter().copied().map(to_auto_track).collect(),
grid_auto_columns: style.grid_auto_columns.iter().copied().map(to_auto_track).collect(),
flex_direction: match style.axis {
Axis::Column => FlexDirection::Column,
Axis::Row => FlexDirection::Row,
},
justify_content: style.justify.map(|j| match j {
Justify::Start => JustifyContent::FlexStart,
Justify::Center => JustifyContent::Center,
Justify::End => JustifyContent::FlexEnd,
Justify::SpaceBetween => JustifyContent::SpaceBetween,
Justify::SpaceAround => JustifyContent::SpaceAround,
}),
align_items: style
.align
.map(to_align_items)
.or(if style.display == Display::Flex {
Some(AlignItems::FlexStart)
} else {
None
}),
align_self: style.align_self.map(to_align_items),
justify_self: style.justify_self.map(to_align_items),
justify_items: style.justify_items.map(to_align_items),
align_content: style.align_content.map(to_align_content),
position: match style.position {
Position::Relative => taffy::Position::Relative,
Position::Absolute => taffy::Position::Absolute,
},
inset: Rect {
left: to_inset(style.inset[3], vp),
right: to_inset(style.inset[1], vp),
top: to_inset(style.inset[0], vp),
bottom: to_inset(style.inset[2], vp),
},
aspect_ratio: style.aspect_ratio,
overflow: match style.overflow {
Overflow::Scroll => Point {
x: taffy::Overflow::Scroll,
y: taffy::Overflow::Scroll,
},
_ => Point {
x: taffy::Overflow::Visible,
y: taffy::Overflow::Visible,
},
},
flex_grow: style.grow,
flex_shrink: style.shrink,
flex_basis: style.basis.map(|l| to_dim(l, vp)).unwrap_or(auto()),
flex_wrap: if style.wrap {
FlexWrap::Wrap
} else {
FlexWrap::NoWrap
},
size: Size {
width: match style.width {
Some(Len::Pct(_)) if style.wrap && style.max_width.is_some() => auto(),
Some(l) => to_dim(l, vp),
None => auto(),
},
height: style.height.map(|l| to_dim(l, vp)).unwrap_or(auto()),
},
min_size: Size {
width: style.min_width.map(|l| to_dim(l, vp)).unwrap_or(auto()),
height: style.min_height.map(|l| to_dim(l, vp)).unwrap_or(auto()),
},
max_size: Size {
width: match (style.max_width, style.width) {
(Some(l), _) => to_dim(l, vp),
(None, None) if style.shrink != 0.0 => percent(1.0_f32),
(None, _) => auto(),
},
height: style.max_height.map(|l| to_dim(l, vp)).unwrap_or(auto()),
},
padding: Rect {
left: length(style.padding.left),
right: length(style.padding.right),
top: length(style.padding.top),
bottom: length(style.padding.bottom),
},
margin: Rect {
left: length(style.margin.left),
right: length(style.margin.right),
top: length(style.margin.top),
bottom: length(style.margin.bottom),
},
border: Rect {
left: length(style.border.left),
right: length(style.border.right),
top: length(style.border.top),
bottom: length(style.border.bottom),
},
gap: Size {
width: length(style.column_gap.unwrap_or(style.gap)),
height: length(style.row_gap.unwrap_or(style.gap)),
},
..Default::default()
}
}
fn to_align_items(a: Align) -> AlignItems {
match a {
Align::Start => AlignItems::FlexStart,
Align::Center => AlignItems::Center,
Align::End => AlignItems::FlexEnd,
Align::Stretch => AlignItems::Stretch,
}
}
fn to_align_content(j: Justify) -> AlignContent {
match j {
Justify::Start => AlignContent::FlexStart,
Justify::Center => AlignContent::Center,
Justify::End => AlignContent::FlexEnd,
Justify::SpaceBetween => AlignContent::SpaceBetween,
Justify::SpaceAround => AlignContent::SpaceAround,
}
}
fn to_inset(l: Option<Len>, vp: (f32, f32)) -> LengthPercentageAuto {
match l {
None => auto(),
Some(Len::Px(v)) => length(v),
Some(Len::Pct(p)) => percent(p),
Some(Len::Vw(v)) => length(vp.0 * v / 100.0),
Some(Len::Vh(v)) => length(vp.1 * v / 100.0),
}
}
struct Bound {
id: NodeId,
model: String,
row: Option<String>,
multiline: bool,
options: Option<Vec<String>>,
}
fn width_cap(style: &Style, parent: Option<f32>, vp: (f32, f32)) -> Option<f32> {
let resolve = |l: Len| match l {
Len::Px(px) => Some(px),
Len::Pct(p) => parent.map(|b| b * p),
Len::Vw(v) => Some(vp.0 * v / 100.0),
Len::Vh(v) => Some(vp.1 * v / 100.0),
};
let capped = match (style.width.and_then(resolve), style.max_width.and_then(resolve)) {
(Some(w), Some(m)) => Some(w.min(m)),
(Some(w), None) => Some(w),
(None, Some(m)) => Some(parent.map_or(m, |p| p.min(m))),
(None, None) => parent,
};
match style.min_width.and_then(resolve) {
Some(min) => Some(capped.map_or(min, |c| c.max(min))),
None => capped,
}
}
fn inner_cap(style: &Style, own: Option<f32>) -> Option<f32> {
own.map(|w| {
let horizontal = style.padding.left + style.padding.right + style.border.left + style.border.right;
(w - horizontal).max(0.0)
})
}
#[allow(clippy::too_many_arguments)]
fn build(
tree: &mut TaffyTree<TextContent>,
node: &Node,
paint: &mut Vec<(NodeId, PaintKind)>,
handlers: &mut Vec<(NodeId, String, Cursor, Option<String>)>,
models: &mut Vec<Bound>,
focus_labels: &mut Vec<(NodeId, String, Option<String>)>,
hidden: &mut Vec<NodeId>,
opacities: &mut Vec<(NodeId, f32)>,
scrolls: &mut Vec<NodeId>,
transforms: &mut Vec<(NodeId, Transform)>,
states: &mut Vec<(NodeId, Vec<usize>)>,
access: &mut Vec<(NodeId, Access, Option<String>)>,
vp: (f32, f32),
cap: Option<f32>,
caps: &mut HashMap<NodeId, f32>,
row: Option<&str>,
) -> NodeId {
let own_cap = width_cap(&node.style, cap, vp);
let child_cap = inner_cap(&node.style, own_cap);
let row = node.key.as_deref().or(row);
let id = if let Some(tc) = &node.text {
let id = tree
.new_leaf_with_context(to_taffy(&node.style, vp), tc.clone())
.expect("taffy text leaf");
paint.push((
id,
PaintKind::Box {
bg: node.style.background.clone(),
radius: node.style.radius,
border_width: node.style.border.top,
border_color: node.style.border_color,
clip: node.style.overflow != Overflow::Visible,
shadow: node.style.box_shadow,
},
));
paint.push((id, PaintKind::Text(tc.clone())));
if let Some(c) = child_cap {
caps.insert(id, c);
}
id
} else if let Some(color) = node.tick {
let id = tree.new_leaf(to_taffy(&node.style, vp)).expect("taffy tick");
paint.push((id, PaintKind::Tick(color)));
id
} else if let Some(ic) = &node.image {
let mut ts = to_taffy(&node.style, vp);
if node.style.width.is_none() {
ts.size.width = length(ic.intrinsic.0);
}
if node.style.height.is_none() {
ts.size.height = length(ic.intrinsic.1);
}
let id = tree.new_leaf(ts).expect("taffy image leaf");
paint.push((
id,
PaintKind::Box {
bg: node.style.background.clone(),
radius: node.style.radius,
border_width: node.style.border.top,
border_color: node.style.border_color,
clip: node.style.overflow != Overflow::Visible,
shadow: node.style.box_shadow,
},
));
paint.push((id, PaintKind::Image(ic.clone())));
id
} else {
let children: Vec<NodeId> = node
.children
.iter()
.map(|c| build(tree, c, paint, handlers, models, focus_labels, hidden, opacities, scrolls, transforms, states, access, vp, child_cap, caps, row))
.collect();
let id = if children.is_empty() {
tree.new_leaf(to_taffy(&node.style, vp)).expect("taffy leaf")
} else {
tree.new_with_children(to_taffy(&node.style, vp), &children)
.expect("taffy node")
};
paint.push((
id,
PaintKind::Box {
bg: node.style.background.clone(),
radius: node.style.radius,
border_width: node.style.border.top,
border_color: node.style.border_color,
clip: node.style.overflow != Overflow::Visible,
shadow: node.style.box_shadow,
},
));
id
};
if let Some(handler) = &node.on_tap {
handlers.push((id, handler.clone(), node.style.cursor, node.instance.clone()));
}
if let Some(model) = &node.model {
models.push(Bound {
id,
model: model.clone(),
row: row.map(str::to_string),
multiline: node.multiline,
options: node.options.clone(),
});
}
if let Some(fm) = &node.focus_model {
focus_labels.push((id, fm.clone(), row.map(str::to_string)));
}
if node.hidden {
hidden.push(id);
}
if node.style.opacity < 1.0 {
opacities.push((id, node.style.opacity.max(0.0)));
}
if let Some(tf) = node.style.transform {
transforms.push((id, tf));
}
if node.style.overflow == Overflow::Scroll {
scrolls.push(id);
}
if let Some(path) = &node.state_path {
states.push((id, path.clone()));
}
if node.access.role.is_meaningful() {
access.push((id, node.access.clone(), node.model.clone()));
}
id
}
#[allow(clippy::too_many_arguments)]
fn collect(
tree: &TaffyTree<TextContent>,
id: NodeId,
origin_x: f32,
origin_y: f32,
paint: &[(NodeId, PaintKind)],
handlers: &[(NodeId, String, Cursor, Option<String>)],
models: &[Bound],
focus_labels: &[(NodeId, String, Option<String>)],
hidden: &[NodeId],
opacities: &[(NodeId, f32)],
scrolls: &[NodeId],
transforms: &[(NodeId, Transform)],
states: &[(NodeId, Vec<usize>)],
access: &[(NodeId, Access, Option<String>)],
offsets: &[Offset],
vp: (f32, f32),
inside_scroll: Option<usize>,
out: &mut Layout,
) {
let layout = tree.layout(id).expect("layout");
let x = origin_x + layout.location.x;
let y = origin_y + layout.location.y;
if hidden.contains(&id) {
return;
}
let alpha = opacities
.iter()
.find(|(nid, _)| *nid == id)
.map(|(_, a)| *a)
.unwrap_or(1.0);
if alpha < 1.0 {
out.paints.push(Paint::PushOpacity {
alpha,
width: vp.0,
height: vp.1,
});
}
let transform = transforms.iter().find(|(nid, _)| *nid == id).map(|(_, m)| *m);
if let Some(m) = transform {
let (ox, oy) = (x + layout.size.width / 2.0, y + layout.size.height / 2.0);
out.paints.push(Paint::PushTransform(centre_transform(m, ox, oy)));
}
let mut clip = false;
let mut clip_radius = [0.0; 4];
for (_, kind) in paint.iter().filter(|(nid, _)| *nid == id) {
match kind {
PaintKind::Box {
bg,
radius,
border_width,
border_color,
clip: c,
shadow,
} => {
clip = *c;
clip_radius = *radius;
if let Some(sh) = shadow.filter(|s| !s.inset) {
out.paints.push(Paint::Shadow {
x: x + sh.dx - sh.spread,
y: y + sh.dy - sh.spread,
width: layout.size.width + 2.0 * sh.spread,
height: layout.size.height + 2.0 * sh.spread,
radius: radius.iter().copied().fold(0.0, f32::max),
blur: sh.blur,
color: sh.color,
});
}
let has_border = *border_width > 0.0 && border_color.is_some();
if bg.is_some() || has_border {
out.paints.push(Paint::Rect(PaintRect {
x,
y,
width: layout.size.width,
height: layout.size.height,
background: bg.clone(),
radius: *radius,
border_width: *border_width,
border_color: *border_color,
}));
}
}
PaintKind::Text(tc) => {
let (cx, cy, cw, ch) = content_box(layout);
out.paints.push(Paint::Text(PaintText {
x: x + cx,
y: y + cy,
width: cw,
height: ch,
content: tc.clone(),
}))
}
PaintKind::Tick(color) => out.paints.push(Paint::Tick(PaintTick {
x,
y,
width: layout.size.width,
height: layout.size.height,
color: *color,
})),
PaintKind::Image(ic) => out.paints.push(Paint::Image(PaintImage {
x,
y,
width: layout.size.width,
height: layout.size.height,
content: ic.clone(),
})),
}
}
if let Some((_, model, row)) = focus_labels.iter().find(|(nid, ..)| *nid == id) {
out.focuses.push(FocusRegion {
x,
y,
width: layout.size.width,
height: layout.size.height,
model: model.clone(),
row: row.clone(),
text: None,
multiline: false,
scroll_id: None,
});
}
if let Some((_, node_access, model)) = access.iter().find(|(nid, ..)| *nid == id) {
out.access.push(AccessNode {
x,
y,
width: layout.size.width,
height: layout.size.height,
access: node_access.clone(),
model: model.clone(),
});
}
if let Some((_, path)) = states.iter().find(|(nid, _)| *nid == id) {
out.states.push(StateRegion {
x,
y,
width: layout.size.width,
height: layout.size.height,
path: path.clone(),
});
}
if let Some((_, handler, cursor, instance)) = handlers.iter().find(|(nid, ..)| *nid == id) {
out.hits.push(HitRegion {
x,
y,
width: layout.size.width,
height: layout.size.height,
on_tap: handler.clone(),
cursor: *cursor,
instance: instance.clone(),
});
}
let (fw, fh) = (layout.size.width, layout.size.height);
if let Some(bound) = models.iter().find(|b| b.id == id) {
if let Some(options) = &bound.options {
out.selects.push(SelectRegion {
x,
y,
width: fw,
height: fh,
model: bound.model.clone(),
row: bound.row.clone(),
options: options.clone(),
});
out.focusables.push(FocusItem {
x,
y,
width: fw,
height: fh,
kind: FocusKind::Select {
model: bound.model.clone(),
row: bound.row.clone(),
options: options.clone(),
},
scroll: inside_scroll,
});
} else {
let text = tree
.children(id)
.ok()
.and_then(|kids| kids.first().copied())
.and_then(|kid| {
let child = tree.layout(kid).ok()?;
let content = paint.iter().find_map(|(nid, k)| match k {
PaintKind::Text(tc) if *nid == kid => Some(tc.clone()),
_ => None,
})?;
let (cx, cy, cw, ch) = content_box(child);
Some(PaintText {
x: x + child.location.x + cx,
y: y + child.location.y + cy,
width: cw,
height: ch,
content,
})
});
out.focuses.push(FocusRegion {
x,
y,
width: fw,
height: fh,
model: bound.model.clone(),
row: bound.row.clone(),
text: text.clone(),
multiline: bound.multiline,
scroll_id: scrolls.contains(&id).then(|| out.scrolls.len()),
});
out.focusables.push(FocusItem {
x,
y,
width: fw,
height: fh,
kind: FocusKind::Text {
model: bound.model.clone(),
row: bound.row.clone(),
multiline: bound.multiline,
text,
},
scroll: inside_scroll,
});
}
} else if let Some((_, handler, _, instance)) = handlers.iter().find(|(nid, ..)| *nid == id) {
out.focusables.push(FocusItem {
x,
y,
width: fw,
height: fh,
kind: FocusKind::Activate { on_tap: handler.clone(), instance: instance.clone() },
scroll: inside_scroll,
});
}
if clip {
out.paints.push(Paint::PushClip {
x,
y,
width: layout.size.width,
height: layout.size.height,
radius: clip_radius,
});
}
let mut shift = Offset::default();
let mut child_scroll = inside_scroll;
if scrolls.contains(&id) {
let sid = out.scrolls.len();
child_scroll = Some(sid);
let max = Offset {
x: (layout.content_size.width - layout.size.width).max(0.0),
y: (layout.content_size.height - layout.size.height).max(0.0),
};
shift = offsets.get(sid).copied().unwrap_or_default().clamp_to(max);
out.scrolls.push(ScrollRegion {
id: sid,
x,
y,
width: layout.size.width,
height: layout.size.height,
content_width: layout.content_size.width,
content_height: layout.content_size.height,
max,
});
}
for child in tree.children(id).expect("children") {
collect(
tree,
child,
x - shift.x,
y - shift.y,
paint,
handlers,
models,
focus_labels,
hidden,
opacities,
scrolls,
transforms,
states,
access,
offsets,
vp,
child_scroll,
out,
);
}
if clip {
out.paints.push(Paint::PopClip);
}
if transform.is_some() {
out.paints.push(Paint::PopTransform);
}
if alpha < 1.0 {
out.paints.push(Paint::PopOpacity);
}
}
fn centre_transform(m: Transform, ox: f32, oy: f32) -> Transform {
let [a, b, c, d, e, f] = m;
[
a,
b,
c,
d,
e + ox - a * ox - c * oy,
f + oy - b * ox - d * oy,
]
}
pub fn layout(root: &Node, avail_w: f32, avail_h: f32, measure: &mut Measure) -> Layout {
layout_scrolled(root, avail_w, avail_h, &[], measure)
}
pub fn layout_scrolled(
root: &Node,
avail_w: f32,
avail_h: f32,
offsets: &[Offset],
measure: &mut Measure,
) -> Layout {
let mut tree: TaffyTree<TextContent> = TaffyTree::new();
tree.disable_rounding();
let mut paint = Vec::new();
let mut handlers = Vec::new();
let mut models = Vec::new();
let mut focus_labels = Vec::new();
let mut hidden = Vec::new();
let mut opacities = Vec::new();
let mut scrolls = Vec::new();
let mut transforms = Vec::new();
let mut states = Vec::new();
let mut access = Vec::new();
let vp = (avail_w, avail_h);
let mut caps: HashMap<NodeId, f32> = HashMap::new();
let root_id = build(
&mut tree,
root,
&mut paint,
&mut handlers,
&mut models,
&mut focus_labels,
&mut hidden,
&mut opacities,
&mut scrolls,
&mut transforms,
&mut states,
&mut access,
vp,
Some(avail_w),
&mut caps,
None, );
let mut root_style = to_taffy(&root.style, vp);
root_style.size = Size {
width: length(avail_w),
height: length(avail_h),
};
tree.set_style(root_id, root_style).expect("set root style");
tree.compute_layout_with_measure(
root_id,
Size {
width: AvailableSpace::Definite(avail_w),
height: AvailableSpace::Definite(avail_h),
},
|known, available, id, ctx, _style| {
if let (Some(w), Some(h)) = (known.width, known.height) {
return Size { width: w, height: h };
}
match ctx {
Some(tc) => {
let max = known.width.or(match available.width {
AvailableSpace::Definite(w) => Some(w),
AvailableSpace::MinContent => Some(0.0),
AvailableSpace::MaxContent => None,
});
let cap = caps.get(&id).copied();
let max = match (max, cap) {
(Some(m), Some(c)) => Some(m.min(c)),
(None, Some(c)) => Some(c),
(m, None) => m,
};
let (w, h) = measure(tc, max);
Size {
width: known.width.unwrap_or(w),
height: known.height.unwrap_or(h),
}
}
None => Size {
width: 0.0,
height: 0.0,
},
}
},
)
.expect("compute layout");
let mut out = Layout::default();
collect(
&tree, root_id, 0.0, 0.0, &paint, &handlers, &models, &focus_labels, &hidden, &opacities,
&scrolls, &transforms, &states, &access, offsets, vp, None, &mut out,
);
out
}