From e2d88ab007711f54f5f33e133dcd0ade59ce6219 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 10 Aug 2023 10:34:31 -0600 Subject: [PATCH] Relax bounds on View type parameter This will allow elements to work with non-views. --- crates/collab_ui/src/collab_titlebar_item.rs | 2 +- crates/collab_ui/src/notifications.rs | 5 +- crates/diagnostics/src/diagnostics.rs | 4 +- crates/drag_and_drop/src/drag_and_drop.rs | 14 +- crates/editor/src/items.rs | 2 +- crates/feedback/src/feedback_editor.rs | 2 +- crates/gpui/playground/src/playground.rs | 63 + .../playground/ui/src/editor_layout_demo.rs | 165 ++ crates/gpui/playground/ui/src/frame.rs | 1565 +++++++++++++++++ .../gpui/playground/ui/src/playground_ui.rs | 200 +++ crates/gpui/src/app.rs | 208 ++- crates/gpui/src/app/window.rs | 15 +- crates/gpui/src/elements.rs | 29 +- crates/gpui/src/elements/align.rs | 8 +- crates/gpui/src/elements/canvas.rs | 5 +- crates/gpui/src/elements/clipped.rs | 8 +- crates/gpui/src/elements/constrained_box.rs | 12 +- crates/gpui/src/elements/container.rs | 9 +- crates/gpui/src/elements/empty.rs | 4 +- crates/gpui/src/elements/expanded.rs | 8 +- crates/gpui/src/elements/flex.rs | 16 +- crates/gpui/src/elements/hook.rs | 9 +- crates/gpui/src/elements/image.rs | 4 +- crates/gpui/src/elements/keystroke_label.rs | 2 +- crates/gpui/src/elements/label.rs | 4 +- crates/gpui/src/elements/list.rs | 32 +- .../gpui/src/elements/mouse_event_handler.rs | 8 +- crates/gpui/src/elements/overlay.rs | 8 +- crates/gpui/src/elements/resizable.rs | 8 +- crates/gpui/src/elements/stack.rs | 13 +- crates/gpui/src/elements/svg.rs | 6 +- crates/gpui/src/elements/text.rs | 4 +- crates/gpui/src/elements/tooltip.rs | 10 +- crates/gpui/src/elements/uniform_list.rs | 11 +- crates/gpui/src/scene/mouse_region.rs | 46 +- crates/language_tools/src/lsp_log.rs | 2 +- crates/language_tools/src/syntax_tree_view.rs | 2 +- crates/project_panel/src/project_panel.rs | 2 +- .../src/highlighted_workspace_location.rs | 4 +- crates/search/src/project_search.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- crates/theme/src/ui.rs | 16 +- crates/welcome/src/welcome.rs | 2 +- crates/workspace/src/item.rs | 4 +- crates/workspace/src/pane.rs | 6 +- .../src/pane/dragged_item_receiver.rs | 4 +- crates/workspace/src/shared_screen.rs | 2 +- 47 files changed, 2271 insertions(+), 286 deletions(-) create mode 100644 crates/gpui/playground/src/playground.rs create mode 100644 crates/gpui/playground/ui/src/editor_layout_demo.rs create mode 100644 crates/gpui/playground/ui/src/frame.rs create mode 100644 crates/gpui/playground/ui/src/playground_ui.rs diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index dbcbaf6072..eadc322165 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1218,7 +1218,7 @@ impl CollabTitlebarItem { style } - fn render_face( + fn render_face( avatar: Arc, avatar_style: AvatarStyle, background_color: Color, diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index cbd072fe89..d38258ce63 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -2,14 +2,14 @@ use client::User; use gpui::{ elements::*, platform::{CursorStyle, MouseButton}, - AnyElement, Element, View, ViewContext, + AnyElement, Element, ViewContext, }; use std::sync::Arc; enum Dismiss {} enum Button {} -pub fn render_user_notification( +pub fn render_user_notification( user: Arc, title: &'static str, body: Option<&'static str>, @@ -19,7 +19,6 @@ pub fn render_user_notification( ) -> AnyElement where F: 'static + Fn(&mut V, &mut ViewContext), - V: View, { let theme = theme::current(cx).clone(); let theme = &theme.contact_notification; diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 16a7340fae..0e5b714f09 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -538,7 +538,7 @@ impl ProjectDiagnosticsEditor { } impl Item for ProjectDiagnosticsEditor { - fn tab_content( + fn tab_content( &self, _detail: Option, style: &theme::Tab, @@ -735,7 +735,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { }) } -pub(crate) fn render_summary( +pub(crate) fn render_summary( summary: &DiagnosticSummary, text_style: &TextStyle, theme: &theme::ProjectDiagnostics, diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index ddfed0c858..28b876504c 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -11,7 +11,7 @@ use gpui::{ const DEAD_ZONE: f32 = 4.; -enum State { +enum State { Down { region_offset: Vector2F, region: RectF, @@ -31,7 +31,7 @@ enum State { Canceled, } -impl Clone for State { +impl Clone for State { fn clone(&self) -> Self { match self { &State::Down { @@ -68,12 +68,12 @@ impl Clone for State { } } -pub struct DragAndDrop { +pub struct DragAndDrop { containers: HashSet>, currently_dragged: Option>, } -impl Default for DragAndDrop { +impl Default for DragAndDrop { fn default() -> Self { Self { containers: Default::default(), @@ -82,7 +82,7 @@ impl Default for DragAndDrop { } } -impl DragAndDrop { +impl DragAndDrop { pub fn register_container(&mut self, handle: WeakViewHandle) { self.containers.insert(handle); } @@ -291,7 +291,7 @@ impl DragAndDrop { } } -pub trait Draggable { +pub trait Draggable { fn as_draggable( self, payload: P, @@ -301,7 +301,7 @@ pub trait Draggable { Self: Sized; } -impl Draggable for MouseEventHandler { +impl Draggable for MouseEventHandler { fn as_draggable( self, payload: P, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b99977a60e..b98b54fe44 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -561,7 +561,7 @@ impl Item for Editor { } } - fn tab_content( + fn tab_content( &self, detail: Option, style: &theme::Tab, diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index 47cb90875a..a717223f6d 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -268,7 +268,7 @@ impl Item for FeedbackEditor { Some("Send Feedback".into()) } - fn tab_content( + fn tab_content( &self, _: Option, style: &theme::Tab, diff --git a/crates/gpui/playground/src/playground.rs b/crates/gpui/playground/src/playground.rs new file mode 100644 index 0000000000..e65277cf03 --- /dev/null +++ b/crates/gpui/playground/src/playground.rs @@ -0,0 +1,63 @@ +use gpui::{ + platform::{TitlebarOptions, WindowOptions}, + AnyElement, Element, Entity, View, +}; +use log::LevelFilter; +use simplelog::SimpleLogger; +use std::ops::{Deref, DerefMut}; + +// dymod! { +// #[path = "../ui/src/playground_ui.rs"] +// pub mod ui { +// // fn workspace(theme: &ThemeColors) -> impl Element; +// } +// } + +fn main() { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + + gpui::App::new(()).unwrap().run(|cx| { + cx.platform().activate(true); + cx.add_window( + WindowOptions { + titlebar: Some(TitlebarOptions { + appears_transparent: true, + ..Default::default() + }), + ..Default::default() + }, + |_| Playground::default(), + ); + }); +} + +#[derive(Clone, Default)] +struct Playground(playground_ui::Playground); + +impl Deref for Playground { + type Target = playground_ui::Playground; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Playground { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Entity for Playground { + type Event = (); +} + +impl View for Playground { + fn ui_name() -> &'static str { + "PlaygroundView" + } + + fn render(&mut self, _: &mut gpui::ViewContext) -> AnyElement { + self.0.clone().into_any() + } +} diff --git a/crates/gpui/playground/ui/src/editor_layout_demo.rs b/crates/gpui/playground/ui/src/editor_layout_demo.rs new file mode 100644 index 0000000000..c51ca0ba92 --- /dev/null +++ b/crates/gpui/playground/ui/src/editor_layout_demo.rs @@ -0,0 +1,165 @@ +use gpui::{AnyElement, Element, LayoutContext, View, ViewContext}; + +#[derive(Element, Clone, Default)] +pub struct Playground(PhantomData); + +// example layout design here: https://www.figma.com/file/5QLTmxjO0xQpDD3CD4hR6T/Untitled?type=design&node-id=0%3A1&mode=design&t=SoJieVVIvDDDKagv-1 + +impl Playground { + pub fn render(&mut self, _: &mut V, _: &mut gpui::ViewContext) -> impl Element { + col() // fullscreen container with header and main in it + .width(flex(1.)) + .height(flex(1.)) + .fill(colors(gray.900)) + .children([ + row() // header container + .fill(colors(gray.900)) + .width(flex(1.)) + .children([ + row() // tab bar + .width(flex(1.)) + .gap(spacing(2)) + .padding(spacing(3)) + .overflow_x(scroll()) + .chidren([ + row() // tab + .padding_x(spacing(3)) + .padding_y(spacing(2)) + .corner_radius(6.) + .gap(spacing(3)) + .align(center()) + .fill(colors(gray.800)) + .children([text("Tab title 1"), svg("icon_name")]), + row() // tab + .padding_x(spacing(3)) + .padding_y(spacing(2)) + .corner_radius(6.) + .gap(spacing(3)) + .align(center()) + .fill(colors(gray.800)) + .children([text("Tab title 2"), svg("icon_name")]), + row() // tab + .padding_x(spacing(3)) + .padding_y(spacing(2)) + .corner_radius(6.) + .gap(spacing(3)) + .align(center()) + .fill(colors(gray.800)) + .children([text("Tab title 3"), svg("icon_name")]), + ]), + row() // tab bar actions + .border_left(colors(gray.700)) + .gap(spacing(2)) + .padding(spacing(3)) + .chidren([ + row() + .width(spacing(8)) + .height(spacing(8)) + .corner_radius(6.) + .justify(center()) + .align(center()) + .fill(colors(gray.800)) + .child(svg(icon_name)), + row() + .width(spacing(8)) + .height(spacing(8)) + .corner_radius(6.) + .justify(center()) + .align(center()) + .fill(colors(gray.800)) + .child(svg(icon_name)), + row() + .width(spacing(8)) + .height(spacing(8)) + .corner_radius(6.) + .justify(center()) + .align(center()) + .fill(colors(gray.800)) + .child(svg(icon_name)), + ]), + ]), + row() // main container + .width(flex(1.)) + .height(flex(1.)) + .children([ + col() // left sidebar + .fill(colors(gray.800)) + .border_right(colors(gray.700)) + .height(flex(1.)) + .width(260.) + .children([ + col() // containter to hold list items and notification alert box + .justify(between()) + .padding_x(spacing(6)) + .padding_bottom(3) + .padding_top(spacing(6)) + .children([ + col().gap(spacing(3)).children([ // sidebar list + text("Item"), + text("Item"), + text("Item"), + text("Item"), + text("Item"), + text("Item"), + text("Item"), + text("Item"), + ]), + col().align(center()).gap(spacing(1)).children([ // notification alert box + text("Title text").size("lg"), + text("Description text goes here") + .text_color(colors(rose.200)), + ]), + ]), + row() + .padding_x(spacing(3)) + .padding_y(spacing(2)) + .border_top(1., colors(gray.700)) + .align(center()) + .gap(spacing(2)) + .fill(colors(gray.900)) + .children([ + row() // avatar container + .width(spacing(8)) + .height(spacing(8)) + .corner_radius(spacing(8)) + .justify(center()) + .align(center()) + .child(image(image_url)), + text("FirstName Lastname"), // user name + ]), + ]), + col() // primary content container + .align(center()) + .justify(center()) + .child( + col().justify(center()).gap(spacing(8)).children([ // detail container wrapper for center positioning + col() // blue rectangle + .width(rem(30.)) + .height(rem(20.)) + .corner_radius(16.) + .fill(colors(blue.200)), + col().gap(spacing(1)).children([ // center content text items + text("This is a title").size("lg"), + text("This is a description").text_color(colors(gray.500)), + ]), + ]), + ), + col(), // right sidebar + ]), + ]) + } +} + +// row( +// padding(), +// width(), +// fill(), +// ) + +// .width(flex(1.)) +// .height(flex(1.)) +// .justify(end()) +// .align(start()) // default +// .fill(green) +// .child(other_tab_bar()) +// .child(profile_menu()) diff --git a/crates/gpui/playground/ui/src/frame.rs b/crates/gpui/playground/ui/src/frame.rs new file mode 100644 index 0000000000..6f33613578 --- /dev/null +++ b/crates/gpui/playground/ui/src/frame.rs @@ -0,0 +1,1565 @@ +#![allow(unused_variables, dead_code)] + +use derive_more::{Add, Deref, DerefMut}; +use gpui::elements::layout_highlighted_chunks; +use gpui::Entity; +use gpui::{ + color::Color, + fonts::HighlightStyle, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::{json, ToJson}, + scene, + serde_json::Value, + text_layout::{Line, ShapedBoundary}, + AnyElement, AppContext, Element, LayoutContext, PaintContext, Quad, SceneBuilder, + SizeConstraint, View, ViewContext, +}; +use length::{Length, Rems}; +use log::warn; +use optional_struct::*; +use std::{any::Any, borrow::Cow, f32, ops::Range, sync::Arc}; + +use crate::color::Rgba; + +pub struct Frame { + style: FrameStyle, + children: Vec>, + id: Option>, + before_paint: Option)>>, +} + +pub fn column() -> Frame { + Frame::default() +} + +pub fn row() -> Frame { + Frame { + style: FrameStyle { + axis: Axis3d::X, + ..Default::default() + }, + ..Default::default() + } +} + +pub fn stack() -> Frame { + Frame { + style: FrameStyle { + axis: Axis3d::Z, + ..Default::default() + }, + ..Default::default() + } +} + +impl Default for Frame { + fn default() -> Self { + Self { + style: Default::default(), + children: Default::default(), + id: None, + before_paint: None, + } + } +} + +impl Element for Frame { + type LayoutState = FrameLayout; + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + view: &mut V, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let layout = if let Some(axis) = self.style.axis.to_2d() { + self.layout_xy(axis, constraint, cx.rem_pixels(), view, cx) + } else { + todo!() + }; + (layout.size.max(constraint.min), layout) + } + + fn paint( + &mut self, + scene: &mut SceneBuilder, + bounds: RectF, + visible_bounds: RectF, + layout: &mut FrameLayout, + view: &mut V, + cx: &mut PaintContext, + ) -> Self::PaintState { + if let Some(before_paint) = &mut self.before_paint { + before_paint(bounds, layout, cx); + } + + let bounds_center = bounds.size() / 2.; + let bounds_target = bounds_center + (bounds_center * self.style.align.0); + let layout_center = layout.size / 2.; + let layout_target = layout_center + layout_center * self.style.align.0; + let delta = bounds_target - layout_target; + + let aligned_bounds = RectF::new(bounds.origin() + delta, layout.size); + let margined_bounds = RectF::from_points( + aligned_bounds.origin() + vec2f(layout.margins.left, layout.margins.top), + aligned_bounds.lower_right() - vec2f(layout.margins.right, layout.margins.bottom), + ); + + // Paint drop shadow + for shadow in &self.style.shadows { + scene.push_shadow(scene::Shadow { + bounds: margined_bounds + shadow.offset, + corner_radius: self.style.corner_radius, + sigma: shadow.blur, + color: shadow.color, + }); + } + + // // Paint cursor style + // if let Some(hit_bounds) = content_bounds.intersection(visible_bounds) { + // if let Some(style) = self.style.cursor { + // scene.push_cursor_region(CursorRegion { + // bounds: hit_bounds, + // style, + // }); + // } + // } + + // Render the background and/or the border. + let Fill::Color(fill_color) = self.style.fill; + let is_fill_visible = fill_color.a > 0.; + if is_fill_visible || self.style.borders.is_visible() { + scene.push_quad(Quad { + bounds: margined_bounds, + background: is_fill_visible.then_some(fill_color.into()), + border: scene::Border { + width: self.style.borders.width, + color: self.style.borders.color, + overlay: false, + top: self.style.borders.top, + right: self.style.borders.right, + bottom: self.style.borders.bottom, + left: self.style.borders.left, + }, + corner_radius: self.style.corner_radius, + }); + } + + if !self.children.is_empty() { + // Account for padding first. + let borders = &self.style.borders; + let padded_bounds = RectF::from_points( + margined_bounds.origin() + + vec2f( + borders.left_width() + layout.padding.left, + borders.top_width() + layout.padding.top, + ), + margined_bounds.lower_right() + - vec2f( + layout.padding.right + borders.right_width(), + layout.padding.bottom + borders.bottom_width(), + ), + ); + + if let Some(axis) = self.style.axis.to_2d() { + // let parent_size = padded_bounds.size(); + let mut child_origin = padded_bounds.origin(); + + for child in &mut self.children { + child.paint(scene, child_origin, visible_bounds, view, cx); + + // Advance along the primary axis by the size of this child + child_origin.set(axis, child_origin.get(axis) + child.size().get(axis)); + } + } else { + todo!(); + } + } + } + + fn rect_for_text_range( + &self, + range_utf16: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &ViewContext, + ) -> Option { + self.children + .iter() + .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx)) + } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &ViewContext, + ) -> Value { + json!({ + "type": "Frame", + "bounds": bounds.to_json(), + // TODO! + // "children": self.content.iter().map(|child| child.debug(view, cx)).collect::>() + }) + } + + fn metadata(&self) -> Option<&dyn Any> { + Some(&self.style) + } +} + +impl Frame { + pub fn id(mut self, id: impl Into>) -> Self { + self.id = Some(id.into()); + self + } + + pub fn child(mut self, child: impl Element) -> Self { + self.children.push(child.into_any()); + self + } + + pub fn children(mut self, children: I) -> Self + where + I: IntoIterator, + E: Element, + { + self.children + .extend(children.into_iter().map(|child| child.into_any())); + self + } + + pub fn size(self, size: impl Into>) -> Self { + let size = size.into(); + self.width(size.width).height(size.height) + } + + pub fn width(mut self, width: impl Into) -> Self { + self.style.size.width = width.into(); + self + } + + pub fn height(mut self, height: impl Into) -> Self { + self.style.size.height = height.into(); + self + } + + pub fn fill(mut self, fill: impl Into) -> Self { + self.style.fill = fill.into(); + self + } + + pub fn text_size(mut self, text_size: Rems) -> Self { + self.style.text.size = Some(text_size); + self + } + + pub fn margins(mut self, margins: impl Into>) -> Self { + self.style.margins = margins.into(); + self + } + + pub fn margin_x(mut self, margin: impl Into) -> Self { + self.style.margins.set_x(margin.into()); + self + } + + pub fn margin_y(mut self, margin: impl Into) -> Self { + self.style.margins.set_y(margin.into()); + self + } + + pub fn margin_top(mut self, top: Length) -> Self { + self.style.margins.top = top; + self + } + + pub fn margin_bottom(mut self, bottom: Length) -> Self { + self.style.margins.bottom = bottom; + self + } + + pub fn margin_left(mut self, left: impl Into) -> Self { + self.style.margins.left = left.into(); + self + } + + pub fn margin_right(mut self, right: impl Into) -> Self { + self.style.margins.right = right.into(); + self + } + + pub fn align(mut self, alignment: f32) -> Self { + let cross_axis = self + .style + .axis + .to_2d() + .map(Axis2d::rotate) + .unwrap_or(Axis2d::Y); + self.style.align.set(cross_axis, alignment); + self + } + + pub fn justify(mut self, alignment: f32) -> Self { + let axis = self.style.axis.to_2d().unwrap_or(Axis2d::X); + self.style.align.set(axis, alignment); + self + } + + fn id_string(&self) -> String { + self.id.as_deref().unwrap_or("").to_string() + } + + fn layout_xy( + &mut self, + primary_axis: Axis2d, + constraint: SizeConstraint, + rem_pixels: f32, + view: &mut V, + cx: &mut LayoutContext, + ) -> FrameLayout { + let cross_axis = primary_axis.rotate(); + let total_flex = self.style.flex(); + let mut layout = FrameLayout { + size: Default::default(), + padding: self.style.padding.fixed_pixels(rem_pixels), + margins: self.style.margins.fixed_pixels(rem_pixels), + borders: self.style.borders.edges(), + }; + let fixed_padding_size = layout.padding.size(); + let fixed_margin_size = layout.margins.size(); + let borders_size = layout.borders.size(); + let fixed_constraint = constraint - fixed_margin_size - borders_size - fixed_padding_size; + + // Determine the child constraints in each dimension based on the styled size + let mut child_constraint = SizeConstraint::default(); + for axis in [Axis2d::X, Axis2d::Y] { + let length = self.style.size.get(axis); + let content_length = match length { + Length::Hug => { + // Tell the children not to expand + 0. + } + Length::Fixed(fixed_length) => { + // Tell the children to expand up to the fixed length minus the padding. + fixed_length.to_pixels(rem_pixels) - fixed_padding_size.get(axis) + } + Length::Auto { .. } => { + // Tell the children to expand to fill their share of the flex space in this node. + length.flex_pixels( + rem_pixels, + &mut total_flex.get(axis), + &mut fixed_constraint.max.get(axis), + ) + } + }; + child_constraint.max.set(axis, content_length); + if axis == cross_axis { + child_constraint.min.set(axis, content_length); + } + } + + // Lay out inflexible children. Total up flex of flexible children for + // use in a second pass. + let mut remaining_length = child_constraint.max.get(primary_axis); + let mut remaining_flex = 0.; + let mut total_length = 0.; + let mut cross_axis_max: f32 = 0.; + + for child in &mut self.children { + if let Some(child_flex) = child + .metadata::() + .map(|style| style.flex().get(primary_axis)) + { + if child_flex > 0. { + remaining_flex += child_flex; + continue; + } + } + + let child_size = child.layout(child_constraint, view, cx); + let child_length = child_size.get(primary_axis); + remaining_length -= child_length; + total_length += child_length; + cross_axis_max = cross_axis_max.max(child_size.get(cross_axis)); + } + + // Distribute the remaining length among the flexible children. + for child in &mut self.children { + if let Some(child_flex) = child + .metadata::() + .map(|style| style.flex().get(primary_axis)) + { + if child_flex > 0. { + let max_child_length = (child_flex / remaining_flex) * remaining_length; + child_constraint.max.set(primary_axis, max_child_length); + + let child_size = child.layout(child_constraint, view, cx); + let child_length = child_size.get(primary_axis); + total_length += child_length; + remaining_length -= child_length; + remaining_flex -= child_flex; + cross_axis_max = cross_axis_max.max(child_size.get(cross_axis)); + } + } + } + + let content_size = match primary_axis { + Axis2d::X => vec2f(total_length, cross_axis_max), + Axis2d::Y => vec2f(cross_axis_max, total_length), + }; + + // Distribute remaining space to flexible padding and margins. + for axis in [Axis2d::X, Axis2d::Y] { + let length = self.style.size.get(axis); + match length { + Length::Hug => { + let mut remaining_flex = total_flex.get(axis); + let mut remaining_length = + fixed_constraint.min.get(axis) - content_size.get(axis); + + layout.padding.compute_flex_edges( + &self.style.padding, + axis, + &mut remaining_flex, + &mut remaining_length, + rem_pixels, + ); + layout.margins.compute_flex_edges( + &self.style.margins, + axis, + &mut remaining_flex, + &mut remaining_length, + rem_pixels, + ); + layout.size.set( + axis, + content_size.get(axis) + + layout.padding.size().get(axis) + + layout.borders.size().get(axis) + + layout.margins.size().get(axis), + ); + } + Length::Fixed(fixed_length) => { + let fixed_length = fixed_length.to_pixels(rem_pixels); + + // With a fixed length, we can only distribute the space in the fixed-length container + // not consumed by the content. + let mut padding_flex = self.style.padding.flex().get(axis); + let mut max_padding_length = (fixed_length - content_size.get(axis)).max(0.); + layout.padding.compute_flex_edges( + &self.style.padding, + axis, + &mut padding_flex, + &mut max_padding_length, + rem_pixels, + ); + + // Similarly, distribute the available space for margins so we preserve the fixed length + // of the container. + let mut margin_flex = self.style.margins.flex().get(axis); + let mut max_margin_length = constraint.max.get(axis) - fixed_length; + layout.margins.compute_flex_edges( + &self.style.margins, + axis, + &mut margin_flex, + &mut max_margin_length, + rem_pixels, + ); + + layout + .size + .set(axis, fixed_length + layout.margins.size().get(axis)) + } + Length::Auto { .. } => { + let mut remaining_flex = total_flex.get(axis); + let mut remaining_length = fixed_constraint.max.get(axis); + let flex_length = + length.flex_pixels(rem_pixels, &mut remaining_flex, &mut remaining_length); + + layout.padding.compute_flex_edges( + &self.style.padding, + axis, + &mut remaining_flex, + &mut remaining_length, + rem_pixels, + ); + + layout.margins.compute_flex_edges( + &self.style.margins, + axis, + &mut remaining_flex, + &mut remaining_length, + rem_pixels, + ); + + layout.size.set( + axis, + flex_length + + layout.padding.size().get(axis) + + layout.borders.size().get(axis) + + layout.margins.size().get(axis), + ) + } + } + } + + layout + } + + fn before_paint(mut self, handler: H) -> Self + where + H: 'static + FnMut(RectF, &mut FrameLayout, &mut PaintContext), + { + self.before_paint = Some(Box::new(handler)); + self + } +} + +pub struct TopBottom { + top: Length, + bottom: Length, +} + +impl> From<(T, T)> for TopBottom { + fn from((top, bottom): (T, T)) -> Self { + Self { + top: top.into(), + bottom: bottom.into(), + } + } +} + +impl> From for TopBottom { + fn from(both: T) -> Self { + Self { + top: both.into(), + bottom: both.into(), + } + } +} + +pub struct LeftRight { + left: Length, + right: Length, +} + +impl From<(Length, Length)> for LeftRight { + fn from((left, right): (Length, Length)) -> Self { + Self { left, right } + } +} + +impl From for LeftRight { + fn from(both: Length) -> Self { + Self { + left: both, + right: both, + } + } +} + +struct Interactive