diff --git a/crates/yakui-core/src/widget.rs b/crates/yakui-core/src/widget.rs index a601e692..f82fa3f7 100644 --- a/crates/yakui-core/src/widget.rs +++ b/crates/yakui-core/src/widget.rs @@ -5,7 +5,7 @@ use std::fmt; use glam::Vec2; -use crate::dom::Dom; +use crate::dom::{Dom, DomNode}; use crate::event::EventResponse; use crate::event::{EventInterest, WidgetEvent}; use crate::geometry::{Constraints, FlexFit}; @@ -119,6 +119,26 @@ pub trait Widget: 'static + fmt::Debug { self.default_layout(ctx, constraints) } + /// Tells the intrinsic width of the object, which is its width if the + /// widget were given unbounded constraints. + fn intrinsic_width(&self,node:&DomNode,dom:&Dom) -> f32 { + self.default_intrinsic_width(node, dom) + } + + /// Default implementation of intrinsic width calculation. + /// Calculates the maximum of child widths + fn default_intrinsic_width(&self,node:&DomNode,dom:&Dom) -> f32 { + let mut width:f32 = 0.0; + + for &child in &node.children { + let node=dom.get(child).unwrap(); + let child_width=node.widget.intrinsic_width(&node,dom); + width = width.max(child_width); + + } + width + } + /// A convenience method that always performs the default layout strategy /// for a widget. This method is intended to be called from custom widget's /// `layout` methods. @@ -183,6 +203,9 @@ pub trait ErasedWidget: Any + fmt::Debug { /// See [`Widget::flex`]. fn flex(&self) -> (u32, FlexFit); + /// See [`Widget::intrinsic_width`]. + fn intrinsic_width(&self,node:&DomNode,dom:&Dom) -> f32; + /// See [`Widget::flow`]. fn flow(&self) -> Flow; @@ -195,6 +218,7 @@ pub trait ErasedWidget: Any + fmt::Debug { /// See [`Widget::event`]. fn event(&mut self, ctx: EventContext<'_>, event: &WidgetEvent) -> EventResponse; + /// Returns the type name of the widget, usable only for debugging. fn type_name(&self) -> &'static str; } @@ -211,6 +235,10 @@ where ::flex(self) } + fn intrinsic_width(&self,node:&DomNode,dom:&Dom) -> f32 { + ::intrinsic_width(self,node,dom) + } + fn flow(&self) -> Flow { ::flow(self) } diff --git a/crates/yakui-widgets/src/shorthand.rs b/crates/yakui-widgets/src/shorthand.rs index bfcbb68f..b2dbe2f4 100644 --- a/crates/yakui-widgets/src/shorthand.rs +++ b/crates/yakui-widgets/src/shorthand.rs @@ -14,9 +14,10 @@ use crate::widgets::{ CheckboxResponse, Circle, CircleResponse, ColoredBox, ColoredBoxResponse, ConstrainedBox, ConstrainedBoxResponse, CountGrid, Divider, DividerResponse, Draggable, DraggableResponse, Flexible, FlexibleResponse, Image, ImageResponse, List, ListResponse, MaxWidth, - MaxWidthResponse, NineSlice, Offset, OffsetResponse, Opaque, OpaqueResponse, Pad, PadResponse, - Reflow, ReflowResponse, Scrollable, ScrollableResponse, Slider, SliderResponse, Spacer, Stack, - StackResponse, State, StateResponse, Text, TextBox, TextBoxResponse, TextResponse, + MaxWidthResponse, NineSlice, Offset, OffsetResponse, Opaque, OpaqueResponse, Outline, + OutlineSide, Pad, PadResponse, Reflow, ReflowResponse, Scrollable, ScrollableResponse, Slider, + SliderResponse, Spacer, Stack, StackResponse, State, StateResponse, Text, TextBox, + TextBoxResponse, TextResponse, }; /// See [List]. @@ -197,6 +198,15 @@ pub fn stack(children: impl FnOnce()) -> Response { Stack::new().show(children) } +/// See [Outline]. +pub fn outline( + color: Color, + width: f32, + side: OutlineSide, + children: F, +) -> Response<()> { + Outline::new(color, width, side).show(children) +} pub fn use_state(default: F) -> Response> where F: FnOnce() -> T + 'static, diff --git a/crates/yakui-widgets/src/widgets/colored_box.rs b/crates/yakui-widgets/src/widgets/colored_box.rs index efd65cee..d8d8cba0 100644 --- a/crates/yakui-widgets/src/widgets/colored_box.rs +++ b/crates/yakui-widgets/src/widgets/colored_box.rs @@ -1,3 +1,4 @@ +use yakui_core::dom::{Dom, DomNode}; use yakui_core::geometry::{Color, Constraints, Vec2}; use yakui_core::paint::PaintRect; use yakui_core::widget::{LayoutContext, PaintContext, Widget}; @@ -47,6 +48,7 @@ impl ColoredBox { pub fn show_children(self, children: F) -> Response { widget_children::(children, self) } + } #[derive(Debug)] @@ -82,6 +84,10 @@ impl Widget for ColoredBoxWidget { input.constrain_min(size) } + fn intrinsic_width(&self,node:&DomNode,dom:&Dom) -> f32 { + self.props.min_size.x.max(self.default_intrinsic_width(node,dom)) + } + fn paint(&self, mut ctx: PaintContext<'_>) { let node = ctx.dom.get_current(); let layout_node = ctx.layout.get(ctx.dom.current()).unwrap(); diff --git a/crates/yakui-widgets/src/widgets/constrained_box.rs b/crates/yakui-widgets/src/widgets/constrained_box.rs index 57bda231..1f2461c8 100644 --- a/crates/yakui-widgets/src/widgets/constrained_box.rs +++ b/crates/yakui-widgets/src/widgets/constrained_box.rs @@ -1,3 +1,4 @@ +use yakui_core::dom::{Dom, DomNode}; use yakui_core::geometry::{Constraints, Vec2}; use yakui_core::widget::{LayoutContext, Widget}; use yakui_core::Response; @@ -65,4 +66,7 @@ impl Widget for ConstrainedBoxWidget { input.constrain(constraints.constrain(size)) } + fn intrinsic_width(&self,node:&DomNode,dom:&Dom) -> f32 { + self.props.constraints.min.x.max(self.default_intrinsic_width(node,dom)) + } } diff --git a/crates/yakui-widgets/src/widgets/image.rs b/crates/yakui-widgets/src/widgets/image.rs index 099f1afc..cb80718c 100644 --- a/crates/yakui-widgets/src/widgets/image.rs +++ b/crates/yakui-widgets/src/widgets/image.rs @@ -2,7 +2,7 @@ use yakui_core::geometry::{Color, Constraints, Rect, Vec2}; use yakui_core::paint::PaintRect; use yakui_core::widget::{LayoutContext, PaintContext, Widget}; use yakui_core::{Response, TextureId}; - +use yakui_core::dom::{Dom, DomNode}; use crate::util::widget; /** @@ -75,4 +75,8 @@ impl Widget for ImageWidget { rect.add(ctx.paint); } } + + fn intrinsic_width(&self, node: &DomNode, dom: &Dom) -> f32 { + self.props.size.x + } } diff --git a/crates/yakui-widgets/src/widgets/intrinsic_width.rs b/crates/yakui-widgets/src/widgets/intrinsic_width.rs new file mode 100644 index 00000000..dbf2d78c --- /dev/null +++ b/crates/yakui-widgets/src/widgets/intrinsic_width.rs @@ -0,0 +1,76 @@ +use yakui_core::dom::Dom; +use yakui_core::geometry::{Constraints, FlexFit, Vec2}; +use yakui_core::widget::{LayoutContext, Widget}; +use yakui_core::Response; + +use crate::util::widget_children; + +/** +A container that sizes its child to the child's intrinsic width + +Responds with [IntrinsicWidthResponse]. + +Shorthand: +```rust + +``` + */ +#[derive(Debug)] +#[non_exhaustive] +#[must_use = "yakui widgets do nothing if you don't `show` them"] +pub struct IntrinsicWidth { + +} + +impl IntrinsicWidth { + pub fn new() -> Self { + Self {} + } + + pub fn show(self, children: F) -> Response { + widget_children::(children, self) + } +} + +#[derive(Debug)] +pub struct IntrinsicWidthWidget { + props: IntrinsicWidth, +} + +pub type IntrinsicWidthResponse = (); + +impl Widget for IntrinsicWidthWidget { + type Props<'a> = IntrinsicWidth; + type Response = IntrinsicWidthResponse; + + fn new() -> Self { + Self { + props: IntrinsicWidth::new(), + } + } + + fn update(&mut self, props: Self::Props<'_>) -> Self::Response { + self.props = props; + } + + fn layout(&self, mut ctx: LayoutContext<'_>, input: Constraints) -> Vec2 { + let node = ctx.dom.get_current(); + + let intrinsic_width=self.intrinsic_width(&node,&ctx.dom); + let constraints = Constraints { + min: (input.min).max(Vec2::ZERO), + max: (input.max).max(Vec2::ZERO).min(Vec2::new(intrinsic_width,input.max.y)), + }; + + let mut size = Vec2::ZERO; + + for &child in &node.children { + let child_size = ctx.calculate_layout(child, constraints); + size = size.max(child_size); + } + + input.constrain(constraints.constrain(size)) + } + + +} diff --git a/crates/yakui-widgets/src/widgets/list.rs b/crates/yakui-widgets/src/widgets/list.rs index 8cb9b935..6cc91b4b 100644 --- a/crates/yakui-widgets/src/widgets/list.rs +++ b/crates/yakui-widgets/src/widgets/list.rs @@ -1,9 +1,9 @@ +use crate::util::widget_children; +use yakui_core::dom::{Dom, DomNode}; use yakui_core::geometry::{Constraints, FlexFit, Vec2}; use yakui_core::widget::{LayoutContext, Widget}; use yakui_core::{CrossAxisAlignment, Direction, Flow, MainAxisAlignment, MainAxisSize, Response}; -use crate::util::widget_children; - /** Lays out children in a single direction. Supports flex sizing. @@ -90,6 +90,24 @@ impl Widget for ListWidget { (flex, FlexFit::Tight) } + fn intrinsic_width(&self,node:&DomNode, dom: &Dom) -> f32 { + if self.props.direction == Direction::Right { + let mut width:f32 = 0.0; + + let total_item_spacing = + self.props.item_spacing * node.children.len().saturating_sub(1) as f32; + + for &child in &node.children { + let node=dom.get(child).unwrap(); + let child_width = node.widget.intrinsic_width(&node,dom); + width += child_width; + } + width+total_item_spacing + } else { + self.default_intrinsic_width(node,dom); + } + } + // This approach to layout is based on Flutter's Flex layout algorithm. // // https://api.flutter.dev/flutter/widgets/Flex-class.html#layout-algorithm diff --git a/crates/yakui-widgets/src/widgets/mod.rs b/crates/yakui-widgets/src/widgets/mod.rs index 2e75a8b7..3cc833e6 100644 --- a/crates/yakui-widgets/src/widgets/mod.rs +++ b/crates/yakui-widgets/src/widgets/mod.rs @@ -32,6 +32,8 @@ mod text; mod textbox; mod unconstrained_box; mod window; +mod outline; +mod intrinsic_width; pub use self::align::*; pub use self::button::*; @@ -67,3 +69,5 @@ pub use self::text::*; pub use self::textbox::*; pub use self::unconstrained_box::*; pub use self::window::*; +pub use self::outline::*; +pub use self::intrinsic_width::*; diff --git a/crates/yakui-widgets/src/widgets/outline.rs b/crates/yakui-widgets/src/widgets/outline.rs new file mode 100644 index 00000000..438117ef --- /dev/null +++ b/crates/yakui-widgets/src/widgets/outline.rs @@ -0,0 +1,76 @@ +use crate::widgets::PadResponse; +use crate::{shapes, shorthand::pad, util::widget_children, widgets::pad::Pad}; +use yakui_core::geometry::Color; +use yakui_core::{ + widget::{PaintContext, Widget}, + Response, +}; + +/** +Applies a colored outline around its children. + */ +#[derive(Debug)] +#[must_use = "yakui widgets do nothing if you don't `show` them"] +pub struct Outline { + color: Color, + width: f32, + side: OutlineSide, +} + +#[derive(Copy, Clone, Debug)] +pub enum OutlineSide { + Inside, + Outside, +} +impl Outline { + pub fn new(color: Color, width: f32, side: OutlineSide) -> Self { + Self { color, width, side } + } + + pub fn show(self, children: impl FnOnce()) -> Response<()> { + let width = self.width; + let side = self.side; + widget_children::( + || match side { + OutlineSide::Inside => { + children(); + } + OutlineSide::Outside => { + pad(Pad::all(width), children); + } + }, + self, + ) + } +} + +#[derive(Debug)] +pub struct OutlineWidget { + props: Option, +} + +impl Widget for OutlineWidget { + type Props<'a> = Outline; + type Response = (); + + fn new() -> Self { + Self { props: None } + } + + fn update(&mut self, props: Self::Props<'_>) -> Self::Response { + self.props = Some(props); + } + + fn paint(&self, mut ctx: PaintContext<'_>) { + let props = self.props.as_ref().unwrap(); + let Outline { color, width, .. } = *props; + + let node = ctx.dom.get_current(); + for &child in &node.children { + ctx.paint(child); + } + + let rect = ctx.layout.get(ctx.dom.current()).unwrap().rect; + shapes::outline(ctx.paint, rect, width, color); + } +} diff --git a/crates/yakui-widgets/src/widgets/pad.rs b/crates/yakui-widgets/src/widgets/pad.rs index 6e70d552..212a6930 100644 --- a/crates/yakui-widgets/src/widgets/pad.rs +++ b/crates/yakui-widgets/src/widgets/pad.rs @@ -1,6 +1,7 @@ +use yakui_core::dom::{Dom, DomNode}; use yakui_core::geometry::{Constraints, Vec2}; use yakui_core::widget::{LayoutContext, Widget}; -use yakui_core::Response; +use yakui_core::{Direction, Response}; use crate::util::widget_children; @@ -110,4 +111,8 @@ impl Widget for PadWidget { self_size = self_size.max(total_padding); input.constrain_min(self_size) } + + fn intrinsic_width(&self, node: &DomNode, dom: &Dom) -> f32 { + self.default_intrinsic_width(node, dom) + self.props.left + self.props.right + } } diff --git a/crates/yakui/examples/layout.rs b/crates/yakui/examples/layout.rs new file mode 100644 index 00000000..a0495570 --- /dev/null +++ b/crates/yakui/examples/layout.rs @@ -0,0 +1,77 @@ +use yakui::widgets::Pad; +use yakui::{button, expanded, pad, Vec2}; +use yakui_core::geometry::Color; +use yakui_core::{Alignment, CrossAxisAlignment, MainAxisAlignment, MainAxisSize}; +use yakui_widgets::widgets::List; + +pub fn run() { + center_ui(|| { + button("test"); + yakui::colored_box(Color::RED, Vec2::new(100.0, 100.0)); + ui_panel(|| { + yakui::colored_box(Color::RED, Vec2::new(100.0, 100.0)); + }); + ui_panel(|| { + yakui::colored_box(Color::GREEN, Vec2::new(100.0, 100.0)); + }); + ui_panel(|| { + let mut l = List::row(); + l.main_axis_size = MainAxisSize::Min; + l.show(|| { + for i in 0..10 { + yakui::flexible(1, || { + pad(Pad::all(8.0), || { + yakui::colored_box(Color::BLUE, Vec2::new(20.0, 20.0)); + }); + }); + } + }); + }); + let mut l = List::row(); + l.main_axis_size = MainAxisSize::Max; + l.main_axis_alignment = MainAxisAlignment::End; + l.show(|| { + expanded( || { + ui_panel(|| { + yakui::colored_box(Color::CORNFLOWER_BLUE, Vec2::new(100.0, 100.0)); + }); + }); + + expanded(|| { + ui_panel(|| { + yakui::colored_box(Color::CORNFLOWER_BLUE, Vec2::new(100.0, 100.0)); + }); + }); + }); + }); +} + +fn main() { + bootstrap::start(run as fn()); +} + +fn center_ui(f: F) { + yakui::align(Alignment::TOP_CENTER, || { + yakui::pad(Pad::all(16.0), || { + yakui::widgets::IntrinsicWidth::new().show(|| { + let mut l = List::column(); + l.main_axis_size = MainAxisSize::Max; + l.cross_axis_alignment = CrossAxisAlignment::Stretch; + l.show(|| { + yakui::spacer(1); + f(); + }); + }); + }); + }); +} + +fn ui_panel(f: F) { + // yakui::flexible(1, || { + yakui::colored_box_container(Color::rgba(255, 255, 0, 128), || { + yakui::pad(Pad::all(8.0), || { + f(); + }); + }); + // }); +} diff --git a/crates/yakui/examples/outline.rs b/crates/yakui/examples/outline.rs new file mode 100644 index 00000000..f55e66b1 --- /dev/null +++ b/crates/yakui/examples/outline.rs @@ -0,0 +1,45 @@ +use yakui::{pad, widgets::Pad}; + +use bootstrap::ExampleState; +use yakui_core::geometry::Color; +use yakui_widgets::widgets::OutlineSide::{Inside, Outside}; +use yakui_widgets::{button, column, outline, text}; + +pub fn run(_state: &mut ExampleState) { + column(|| { + pad(Pad::all(20.0), || { + outline(Color::RED, 5.0, Inside, || { + text(20.0, "internal outline"); + }); + }); + pad(Pad::all(20.0), || { + outline(Color::RED, 5.0, Outside, || { + text(20.0, "external outline"); + }); + }); + pad(Pad::all(20.0), || { + outline(Color::RED, 1.0, Outside, || { + text(20.0, "varying width"); + }); + }); + pad(Pad::all(20.0), || { + outline(Color::RED, 2.0, Outside, || { + text(20.0, "varying width"); + }); + }); + pad(Pad::all(20.0), || { + outline(Color::GREEN, 3.0, Outside, || { + text(20.0, "other colors"); + }); + }); + pad(Pad::all(20.0), || { + outline(Color::GREEN, 3.0, Outside, || { + button("other widgets"); + }); + }); + }); +} + +fn main() { + bootstrap::start(run as fn(&mut ExampleState)); +}