diff --git a/src/view/mod.rs b/src/view/mod.rs index 958f6acc2..b32b43f91 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -21,6 +21,7 @@ mod button; // mod use_state; mod linear_layout; mod list; +mod taffy_layout; #[allow(clippy::module_inception)] mod view; @@ -29,4 +30,5 @@ pub use xilem_core::{Id, IdPath, VecSplice}; pub use button::button; pub use linear_layout::{h_stack, v_stack, LinearLayout}; pub use list::{list, List}; +pub use taffy_layout::{div, flex_column, flex_row, TaffyLayout}; pub use view::{Adapt, AdaptState, Cx, Memoize, View, ViewMarker, ViewSequence}; diff --git a/src/view/taffy_layout.rs b/src/view/taffy_layout.rs new file mode 100644 index 000000000..d378cc858 --- /dev/null +++ b/src/view/taffy_layout.rs @@ -0,0 +1,137 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{any::Any, marker::PhantomData}; + +use crate::view::{Id, VecSplice, ViewMarker, ViewSequence}; +use crate::widget::{self, ChangeFlags}; +use crate::MessageResult; + +mod taffy { + pub use taffy::compute::*; + pub use taffy::style::*; + pub use taffy::style_helpers::*; + pub use taffy::tree::*; +} + +use super::{Cx, View}; + +/// TaffyLayout is a simple view which does layout for the specified ViewSequence. +/// +/// Each Element is positioned on the specified Axis starting at the beginning with the given spacing +/// +/// This View is only temporary is probably going to be replaced by something like Druid's Flex +/// widget. +pub struct TaffyLayout> { + children: VT, + style: taffy::Style, + phantom: PhantomData (T, A)>, +} + +/// creates a vertical [`TaffyLayout`]. +pub fn flex_column>(children: VT) -> TaffyLayout { + TaffyLayout::new_flex(children, taffy::FlexDirection::Column) +} + +/// creates a horizontal [`TaffyLayout`]. +pub fn flex_row>(children: VT) -> TaffyLayout { + TaffyLayout::new_flex(children, taffy::FlexDirection::Row) +} + +/// creates a horizontal [`TaffyLayout`]. +pub fn div>(children: VT) -> TaffyLayout { + TaffyLayout::new(children, taffy::Display::Block) +} + +impl> TaffyLayout { + pub fn new(children: VT, display: taffy::Display) -> Self { + let phantom = Default::default(); + TaffyLayout { + children, + style: taffy::Style { + display, + ..Default::default() + }, + phantom, + } + } + + pub fn new_flex(children: VT, flex_direction: taffy::FlexDirection) -> Self { + let phantom = Default::default(); + let display = taffy::Display::Flex; + TaffyLayout { + children, + style: taffy::Style { + display, + flex_direction, + ..Default::default() + }, + phantom, + } + } + + pub fn with_style(mut self, style_modifier: impl FnOnce(&mut taffy::Style)) -> Self { + style_modifier(&mut self.style); + self + } +} + +impl> ViewMarker for TaffyLayout {} + +impl> View for TaffyLayout { + type State = VT::State; + + type Element = widget::TaffyLayout; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let mut elements = vec![]; + let (id, state) = cx.with_new_id(|cx| self.children.build(cx, &mut elements)); + let column = widget::TaffyLayout::new(elements, self.style.clone()); + (id, state, column) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut scratch = vec![]; + let mut splice = VecSplice::new(&mut element.children, &mut scratch); + + let mut flags = cx.with_id(*id, |cx| { + self.children + .rebuild(cx, &prev.children, state, &mut splice) + }); + + if self.style != prev.style { + element.style = self.style.clone(); + flags |= ChangeFlags::LAYOUT; + } + + flags + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + event: Box, + app_state: &mut T, + ) -> MessageResult { + self.children.message(id_path, state, event, app_state) + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 2ec7572b0..190997223 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -21,6 +21,7 @@ mod core; mod linear_layout; mod piet_scene_helpers; mod raw_event; +mod taffy_layout; //mod scroll_view; //mod text; #[allow(clippy::module_inception)] @@ -33,4 +34,5 @@ pub use button::Button; pub use contexts::{AccessCx, CxState, EventCx, LayoutCx, LifeCycleCx, PaintCx, UpdateCx}; pub use linear_layout::LinearLayout; pub use raw_event::{Event, LifeCycle, MouseEvent, ViewContext}; +pub use taffy_layout::TaffyLayout; pub use widget::{AnyWidget, Widget}; diff --git a/src/widget/taffy_layout.rs b/src/widget/taffy_layout.rs new file mode 100644 index 000000000..d584ee8c1 --- /dev/null +++ b/src/widget/taffy_layout.rs @@ -0,0 +1,398 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::geometry::Axis; +use crate::widget::{AccessCx, BoxConstraints, Event}; +use ::taffy::style::AvailableSpace; +use ::taffy::style_helpers::TaffyMaxContent; +use accesskit::NodeId; +use vello::kurbo::{Point, Size}; +use vello::SceneBuilder; + +use super::{contexts::LifeCycleCx, EventCx, LayoutCx, LifeCycle, PaintCx, Pod, UpdateCx, Widget}; + +mod taffy { + pub use taffy::compute::*; + pub use taffy::geometry::*; + pub use taffy::prelude::*; + pub use taffy::style::*; + pub use taffy::style_helpers::*; + pub use taffy::tree::*; +} + +/// Type inference gets confused because we're just passing None here. So we give +/// it a concrete type to work with (even though we never construct the inner type) +type DummyMeasureFunction = + fn(taffy::Size>, taffy::Size) -> taffy::Size; + +/// TaffyLayout is a simple widget which does layout for a ViewSequence. +/// +/// Each Element is positioned on the specified Axis starting at the beginning with the given spacing +/// +/// This Widget is only temporary and is probably going to be replaced by something like Druid's Flex +/// widget. +pub struct TaffyLayout { + pub children: Vec, + pub child_caches: Vec, + pub child_layouts: Vec, + pub style: taffy::Style, +} + +impl TaffyLayout { + pub fn new(children: Vec, style: taffy::Style) -> Self { + let len = children.len(); + let mut caches = Vec::new(); + caches.resize_with(len, taffy::Cache::new); + TaffyLayout { + children, + child_caches: caches, + child_layouts: vec![taffy::Layout::new(); len], + style, + } + } +} + +struct ChildIter(std::ops::Range); +impl Iterator for ChildIter { + type Item = taffy::NodeId; + + fn next(&mut self) -> Option { + self.0.next().map(taffy::NodeId::from) + } +} + +fn to_taffy_axis(axis: Axis) -> taffy::AbsoluteAxis { + match axis { + Axis::Horizontal => taffy::AbsoluteAxis::Horizontal, + Axis::Vertical => taffy::AbsoluteAxis::Vertical, + } +} + +fn from_taffy_axis(axis: taffy::RequestedAxis) -> Axis { + match axis { + taffy::RequestedAxis::Horizontal => Axis::Horizontal, + taffy::RequestedAxis::Vertical => Axis::Vertical, + // Taffy only uses "both" axis when run mode is PerformLayout. So as long as we only call this function + // when run mode is ComputeSize (which is the only time we care about axes) then this is unreachable. + taffy::RequestedAxis::Both => unreachable!(), + } +} + +fn to_taffy_constraints( + bc: &BoxConstraints, + axis: taffy::RequestedAxis, + run_mode: taffy::RunMode, + sizing_mode: taffy::SizingMode, +) -> taffy::LayoutInput { + let min = bc.min(); + let max = bc.max(); + + taffy::LayoutInput { + known_dimensions: taffy::Size { + width: if min.width == max.width && min.width.is_finite() { + Some(min.width as f32) + } else { + None + }, + height: if min.height == max.height && min.height.is_finite() { + Some(min.height as f32) + } else { + None + }, + }, + parent_size: taffy::Size { + width: if max.width.is_finite() { + Some(max.width as f32) + } else { + None + }, + height: if max.height.is_finite() { + Some(max.height as f32) + } else { + None + }, + }, + available_space: taffy::Size { + width: if max.width.is_finite() { + AvailableSpace::Definite(max.width as f32) + } else { + AvailableSpace::MAX_CONTENT + }, + height: if max.height.is_finite() { + AvailableSpace::Definite(max.height as f32) + } else { + AvailableSpace::MAX_CONTENT + }, + }, + axis, + run_mode, + sizing_mode, + vertical_margins_are_collapsible: taffy::Line::FALSE, + } +} + +fn to_box_constraints(input: &taffy::LayoutInput) -> BoxConstraints { + BoxConstraints::new( + Size { + width: input.known_dimensions.width.unwrap_or(0.0) as f64, + height: input.known_dimensions.height.unwrap_or(0.0) as f64, + }, + Size { + width: input + .known_dimensions + .width + .unwrap_or(match input.available_space.width { + taffy::AvailableSpace::Definite(val) => val, + taffy::AvailableSpace::MinContent => f32::INFINITY, + taffy::AvailableSpace::MaxContent => f32::INFINITY, + }) as f64, + height: input + .known_dimensions + .height + .unwrap_or(match input.available_space.height { + taffy::AvailableSpace::Definite(val) => val, + taffy::AvailableSpace::MinContent => f32::INFINITY, + taffy::AvailableSpace::MaxContent => f32::INFINITY, + }) as f64, + }, + ) +} + +struct TaffyLayoutCtx<'w, 'a, 'b> { + widget: &'w mut TaffyLayout, + cx: &'w mut LayoutCx<'a, 'b>, +} + +impl<'w, 'a, 'b> taffy::PartialLayoutTree for TaffyLayoutCtx<'w, 'a, 'b> { + type ChildIter<'c> = ChildIter where Self: 'c; + + fn child_ids(&self, _parent_node_id: taffy::NodeId) -> Self::ChildIter<'_> { + ChildIter(0..self.widget.children.len()) + } + + fn child_count(&self, _parent_node_id: taffy::NodeId) -> usize { + self.widget.children.len() + } + + fn get_child_id(&self, _parent_node_id: taffy::NodeId, child_index: usize) -> taffy::NodeId { + taffy::NodeId::from(child_index) + } + + fn get_style(&self, node_id: taffy::NodeId) -> &::taffy::style::Style { + let node_id = usize::from(node_id); + if node_id == usize::MAX { + &self.widget.style + } else { + let child = &self.widget.children[node_id]; + match child.downcast_ref::() { + Some(child_widget) => &child_widget.style, + None => { + static DEFAULT_STYLE: taffy::Style = taffy::Style::DEFAULT; + &DEFAULT_STYLE + } + } + } + } + + fn get_unrounded_layout_mut(&mut self, node_id: taffy::NodeId) -> &mut ::taffy::tree::Layout { + &mut self.widget.child_layouts[usize::from(node_id)] + } + + fn get_cache_mut(&mut self, node_id: taffy::NodeId) -> &mut ::taffy::tree::Cache { + &mut self.widget.child_caches[usize::from(node_id)] + } + + fn compute_child_layout( + &mut self, + node_id: taffy::NodeId, + input: ::taffy::tree::LayoutInput, + ) -> ::taffy::tree::LayoutOutput { + let box_constraints: BoxConstraints = to_box_constraints(&input); + match input.run_mode { + taffy::RunMode::PerformLayout => { + let child = &mut self.widget.children[usize::from(node_id)]; + let size = child.layout(self.cx, &box_constraints); + let taffy_size = taffy::Size { + width: size.width as f32, + height: size.height as f32, + }; + taffy::LayoutOutput::from(taffy_size) + } + taffy::RunMode::ComputeSize => { + let axis_size = self.widget.children[usize::from(node_id)].compute_max_intrinsic( + from_taffy_axis(input.axis), + self.cx, + &box_constraints, + ); + let taffy_size = match input.axis { + taffy::RequestedAxis::Horizontal => taffy::Size { + width: axis_size as f32, + height: 0.0, + }, + taffy::RequestedAxis::Vertical => taffy::Size { + width: 0.0, + height: axis_size as f32, + }, + taffy::RequestedAxis::Both => unreachable!(), + }; + taffy::LayoutOutput::from(taffy_size) + } + taffy::RunMode::PerformHiddenLayout => { + // TODO: set size of widget to zero + taffy::LayoutOutput::HIDDEN + } + } + } +} + +impl Widget for TaffyLayout { + fn event(&mut self, cx: &mut EventCx, event: &Event) { + for child in &mut self.children { + child.event(cx, event); + } + } + + fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle) { + for child in &mut self.children { + child.lifecycle(cx, event); + } + } + + fn update(&mut self, cx: &mut UpdateCx) { + for child in &mut self.children { + child.update(cx); + } + } + + fn layout(&mut self, cx: &mut LayoutCx, bc: &BoxConstraints) -> Size { + // We run the following wrapped in "compute_cached_layout", which will check the cache for an entry matching the node and inputs and: + // - Return that entry if exists + // - Else call the passed closure (below) to compute the result + // + // If there was no cache match and a new result needs to be computed then that result will be added to the cache + // compute_cached_layout(self, node, inputs, |widget, node, inputs| { + let display_mode = self.style.display; + let has_children = !self.children.is_empty(); + + let inputs = to_taffy_constraints( + bc, + taffy::RequestedAxis::Both, + taffy::RunMode::PerformLayout, + taffy::SizingMode::InherentSize, + ); + let node_id = taffy::NodeId::from(usize::MAX); + + // Dispatch to a layout algorithm based on the node's display style and whether the node has children or not. + let mut layout_ctx = TaffyLayoutCtx { widget: self, cx }; + let output = match (display_mode, has_children) { + (taffy::Display::None, _) => taffy::compute_hidden_layout(&mut layout_ctx, node_id), + (taffy::Display::Block, true) => { + taffy::compute_block_layout(&mut layout_ctx, node_id, inputs) + } + (taffy::Display::Flex, true) => { + taffy::compute_flexbox_layout(&mut layout_ctx, node_id, inputs) + } + (taffy::Display::Grid, true) => { + taffy::compute_grid_layout(&mut layout_ctx, node_id, inputs) + } + (_, false) => { + let measure_function: Option = None; + taffy::compute_leaf_layout(inputs, &self.style, measure_function) + } + }; + + for (child, layout) in self.children.iter_mut().zip(&self.child_layouts) { + child.set_origin( + cx, + Point { + x: layout.location.x as f64, + y: layout.location.y as f64, + }, + ); + } + cx.request_paint(); + + Size { + width: output.size.width as f64, + height: output.size.height as f64, + } + // }) + } + + fn compute_max_intrinsic(&mut self, axis: Axis, cx: &mut LayoutCx, bc: &BoxConstraints) -> f64 { + // We run the following wrapped in "compute_cached_layout", which will check the cache for an entry matching the node and inputs and: + // - Return that entry if exists + // - Else call the passed closure (below) to compute the result + // + // If there was no cache match and a new result needs to be computed then that result will be added to the cache + // compute_cached_layout(self, node, inputs, |widget, node, inputs| { + let display_mode = self.style.display; + let has_children = !self.children.is_empty(); + + let node_id = taffy::NodeId::from(usize::MAX); + let taffy_axis = to_taffy_axis(axis); + let inputs = to_taffy_constraints( + bc, + taffy_axis.into(), + taffy::RunMode::ComputeSize, + taffy::SizingMode::InherentSize, // TODO: Support SizingMode::ContentSize + ); + + // Dispatch to a layout algorithm based on the node's display style and whether the node has children or not. + let mut layout_ctx = TaffyLayoutCtx { widget: self, cx }; + let output = match (display_mode, has_children) { + (taffy::Display::None, _) => taffy::compute_hidden_layout(&mut layout_ctx, node_id), + (taffy::Display::Block, true) => { + taffy::compute_block_layout(&mut layout_ctx, node_id, inputs) + } + (taffy::Display::Flex, true) => { + taffy::compute_flexbox_layout(&mut layout_ctx, node_id, inputs) + } + (taffy::Display::Grid, true) => { + taffy::compute_grid_layout(&mut layout_ctx, node_id, inputs) + } + (_, false) => { + let measure_function: Option = None; + taffy::compute_leaf_layout(inputs, &self.style, measure_function) + } + }; + + output.size.get_abs(taffy_axis) as f64 + // }) + } + + fn accessibility(&mut self, cx: &mut AccessCx) { + for child in &mut self.children { + child.accessibility(cx); + } + + if cx.is_requested() { + let mut builder = accesskit::NodeBuilder::new(accesskit::Role::GenericContainer); + builder.set_children( + self.children + .iter() + .map(|pod| pod.id().into()) + .collect::>(), + ); + cx.push_node(builder); + } + } + + fn paint(&mut self, cx: &mut PaintCx, builder: &mut SceneBuilder) { + for child in &mut self.children { + println!("paint child!"); + child.paint(cx, builder); + } + } +}