diff --git a/CHANGELOG.md b/CHANGELOG.md index 49df2b4eff..94ea1a8bb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ You can find its changes [documented below](#070---2021-01-01). - `EventCtx::submit_notification_without_warning` ([#2141] by [@xarvic]) - `WidgetPod::requested_layout` ([#2145] by [@xarvic]) - Make `Parse` work better with floats and similar types ([#2148] by [@superfell]) +- Added `compute_max_intrinsic` method to the `Widget` trait, which determines the maximum useful dimension of the widget ([#2172] by [@sjoshid]) ### Changed @@ -845,6 +846,7 @@ Last release without a changelog :( [#2151]: https://github.com/linebender/druid/pull/2151 [#2157]: https://github.com/linebender/druid/pull/2157 [#2158]: https://github.com/linebender/druid/pull/2158 +[#2172]: https://github.com/linebender/druid/pull/2172 [Unreleased]: https://github.com/linebender/druid/compare/v0.7.0...master [0.7.0]: https://github.com/linebender/druid/compare/v0.6.0...v0.7.0 diff --git a/druid/src/box_constraints.rs b/druid/src/box_constraints.rs index ccf8a2af72..6074d457b5 100644 --- a/druid/src/box_constraints.rs +++ b/druid/src/box_constraints.rs @@ -15,6 +15,7 @@ //! The fundamental druid types. use crate::kurbo::Size; +use crate::widget::Axis; /// Constraints for layout. /// @@ -270,6 +271,53 @@ impl BoxConstraints { } } } + + /// Sets the max on a given axis to infinity. + pub fn unbound_max(&self, axis: Axis) -> Self { + match axis { + Axis::Horizontal => self.unbound_max_width(), + Axis::Vertical => self.unbound_max_height(), + } + } + + /// Sets max width to infinity. + pub fn unbound_max_width(&self) -> Self { + let mut max = self.max(); + max.width = f64::INFINITY; + BoxConstraints::new(self.min(), max) + } + + /// Sets max height to infinity. + pub fn unbound_max_height(&self) -> Self { + let mut max = self.max(); + max.height = f64::INFINITY; + BoxConstraints::new(self.min(), max) + } + + /// Shrinks the max dimension on the given axis. + /// Does NOT shrink beyond min. + pub fn shrink_max_to(&self, axis: Axis, dim: f64) -> Self { + match axis { + Axis::Horizontal => self.shrink_max_width_to(dim), + Axis::Vertical => self.shrink_max_height_to(dim), + } + } + + /// Shrinks the max width to dim. + /// Does NOT shrink beyond min width. + pub fn shrink_max_width_to(&self, dim: f64) -> Self { + let mut max = self.max(); + max.width = f64::max(dim, self.min().width); + BoxConstraints::new(self.min(), max) + } + + /// Shrinks the max height to dim. + /// Does NOT shrink beyond min height. + pub fn shrink_max_height_to(&self, dim: f64) -> Self { + let mut max = self.max(); + max.height = f64::max(dim, self.min().height); + BoxConstraints::new(self.min(), max) + } } #[cfg(test)] diff --git a/druid/src/widget/aspect_ratio_box.rs b/druid/src/widget/aspect_ratio_box.rs index 3dbacec85d..dc6b5947f7 100644 --- a/druid/src/widget/aspect_ratio_box.rs +++ b/druid/src/widget/aspect_ratio_box.rs @@ -14,6 +14,7 @@ use crate::debug_state::DebugState; +use crate::widget::Axis; use druid::widget::prelude::*; use druid::Data; use tracing::{instrument, warn}; @@ -171,4 +172,30 @@ impl Widget for AspectRatioBox { ..Default::default() } } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + match axis { + Axis::Horizontal => { + if bc.is_height_bounded() { + bc.max().height * self.ratio + } else { + self.child.compute_max_intrinsic(axis, ctx, bc, data, env) + } + } + Axis::Vertical => { + if bc.is_width_bounded() { + bc.max().width / self.ratio + } else { + self.child.compute_max_intrinsic(axis, ctx, bc, data, env) + } + } + } + } } diff --git a/druid/src/widget/container.rs b/druid/src/widget/container.rs index 79a7633598..7ac7d2a561 100644 --- a/druid/src/widget/container.rs +++ b/druid/src/widget/container.rs @@ -18,6 +18,7 @@ use super::BackgroundBrush; use crate::debug_state::DebugState; use crate::kurbo::RoundedRectRadii; use crate::widget::prelude::*; +use crate::widget::Axis; use crate::{Color, Data, KeyOrValue, Point, WidgetPod}; use tracing::{instrument, trace, trace_span}; @@ -242,4 +243,25 @@ impl Widget for Container { ..Default::default() } } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + let container_width = match &self.border { + Some(border) => border.width.resolve(env), + None => 0.0, + }; + let child_bc = bc.shrink((2.0 * container_width, 2.0 * container_width)); + let child_size = self + .child + .widget_mut() + .compute_max_intrinsic(axis, ctx, &child_bc, data, env); + let border_width_on_both_sides = container_width * 2.; + child_size + border_width_on_both_sides + } } diff --git a/druid/src/widget/controller.rs b/druid/src/widget/controller.rs index d66a6ff0a5..efda30b0f0 100644 --- a/druid/src/widget/controller.rs +++ b/druid/src/widget/controller.rs @@ -16,7 +16,7 @@ use crate::debug_state::DebugState; use crate::widget::prelude::*; -use crate::widget::WidgetWrapper; +use crate::widget::{Axis, WidgetWrapper}; /// A trait for types that modify behaviour of a child widget. /// @@ -142,6 +142,17 @@ impl, C: Controller> Widget for ControllerHost { ..Default::default() } } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + self.widget.compute_max_intrinsic(axis, ctx, bc, data, env) + } } impl WidgetWrapper for ControllerHost { diff --git a/druid/src/widget/flex.rs b/druid/src/widget/flex.rs index 12c23ace64..3c0f5f5b7a 100644 --- a/druid/src/widget/flex.rs +++ b/druid/src/widget/flex.rs @@ -14,6 +14,8 @@ //! A widget that arranges its children in a one-dimensional array. +use std::ops::Add; + use crate::debug_state::DebugState; use crate::kurbo::{common::FloatExt, Vec2}; use crate::widget::prelude::*; @@ -942,6 +944,108 @@ impl Widget for Flex { ..Default::default() } } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + if self.direction != axis { + // Direction axis and sizing axis are different. + // We compute max(child dim in cross axis). + let mut max_size_on_cross_axis: f64 = 0.; + let mut available_size_on_main_axis = self.direction.major(bc.max()); + let mut total_flex = 0.; + for child in self.children.iter_mut() { + match child { + Child::Fixed { widget, .. } => { + let new_bc = bc + .unbound_max(axis) + .shrink_max_to(self.direction, available_size_on_main_axis); + let size_on_main_axis = widget.widget_mut().compute_max_intrinsic( + self.direction, + ctx, + &new_bc, + data, + env, + ); + let new_bc = new_bc.shrink_max_to(self.direction, size_on_main_axis); + let size_on_cross_axis = widget + .widget_mut() + .compute_max_intrinsic(axis, ctx, &new_bc, data, env); + available_size_on_main_axis -= size_on_main_axis; + max_size_on_cross_axis = max_size_on_cross_axis.max(size_on_cross_axis); + } + Child::FixedSpacer(kv, _) => { + let mut s = kv.resolve(env); + if s < 0.0 { + tracing::warn!("Length provided to fixed spacer was less than 0"); + s = 0.; + } + max_size_on_cross_axis = max_size_on_cross_axis.max(s); + } + Child::Flex { flex, .. } | Child::FlexedSpacer(flex, _) => total_flex += *flex, + } + } + let space_per_flex = available_size_on_main_axis / total_flex; + + if space_per_flex > 0.0 { + for child in self.children.iter_mut() { + // We ignore Child::FlexedSpacer because its cross size is irrelevant. + // Its flex matters only on main axis. But here we are interested in cross size of + // each flex child. + if let Child::Flex { widget, flex, .. } = child { + let main_axis_available_space = *flex * space_per_flex; + let new_bc = bc.shrink_max_to(axis, main_axis_available_space); + let size_on_cross_axis = widget + .widget_mut() + .compute_max_intrinsic(axis, ctx, &new_bc, data, env); + max_size_on_cross_axis = max_size_on_cross_axis.max(size_on_cross_axis); + } + } + } + max_size_on_cross_axis + } else { + // Direction axis and sizing axis are same. + // We compute total(child dim on that axis) + let mut total: f64 = 0.; + let mut max_flex_fraction: f64 = 0.; + let mut total_flex = 0.; + for child in self.children.iter_mut() { + match child { + Child::Fixed { widget, .. } => { + let s = widget + .widget_mut() + .compute_max_intrinsic(axis, ctx, bc, data, env); + total = total.add(s); + } + Child::Flex { widget, flex, .. } => { + let s = widget + .widget_mut() + .compute_max_intrinsic(axis, ctx, bc, data, env); + let flex_fraction = s / *flex; + total_flex += *flex; + max_flex_fraction = max_flex_fraction.max(flex_fraction); + } + Child::FixedSpacer(kv, _) => { + let mut s = kv.resolve(env); + if s < 0.0 { + tracing::warn!("Length provided to fixed spacer was less than 0"); + s = 0.; + } + total = total.add(s); + } + Child::FlexedSpacer(flex, _) => { + total_flex += *flex; + } + } + } + total + max_flex_fraction * total_flex + } + } } impl CrossAxisAlignment { diff --git a/druid/src/widget/intrinsic_width.rs b/druid/src/widget/intrinsic_width.rs new file mode 100644 index 0000000000..8daee67181 --- /dev/null +++ b/druid/src/widget/intrinsic_width.rs @@ -0,0 +1,81 @@ +use crate::widget::Axis; +use crate::{ + BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Size, + UpdateCtx, Widget, +}; + +/// A widget that sizes its child to the child's maximum intrinsic width. +/// +/// This widget is useful, for example, when unlimited width is available and you would like a child +/// that would otherwise attempt to expand infinitely to instead size itself to a more reasonable +/// width. +/// +/// The constraints that this widget passes to its child will adhere to the parent's +/// constraints, so if the constraints are not large enough to satisfy the child's maximum intrinsic +/// width, then the child will get less width than it otherwise would. Likewise, if the minimum +/// width constraint is larger than the child's maximum intrinsic width, the child will be given +/// more width than it otherwise would. +pub struct IntrinsicWidth { + child: Box>, +} + +impl IntrinsicWidth { + /// Wrap the given `child` in this widget. + pub fn new(child: impl Widget + 'static) -> Self { + Self { + child: Box::new(child), + } + } +} + +impl Widget for IntrinsicWidth { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + self.child.event(ctx, event, data, env); + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + self.child.lifecycle(ctx, event, data, env); + } + + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { + self.child.update(ctx, old_data, data, env); + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { + let iw = self + .child + .compute_max_intrinsic(Axis::Horizontal, ctx, bc, data, env); + let new_bc = bc.shrink_max_width_to(iw); + + self.child.layout(ctx, &new_bc, data, env) + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { + self.child.paint(ctx, data, env); + } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + match axis { + Axis::Horizontal => self.child.compute_max_intrinsic(axis, ctx, bc, data, env), + Axis::Vertical => { + if !bc.is_width_bounded() { + let w = self + .child + .compute_max_intrinsic(Axis::Horizontal, ctx, bc, data, env); + let new_bc = bc.shrink_max_width_to(w); + self.child + .compute_max_intrinsic(axis, ctx, &new_bc, data, env) + } else { + self.child.compute_max_intrinsic(axis, ctx, bc, data, env) + } + } + } + } +} diff --git a/druid/src/widget/label.rs b/druid/src/widget/label.rs index 29e5df56d6..3c96efac8a 100644 --- a/druid/src/widget/label.rs +++ b/druid/src/widget/label.rs @@ -23,11 +23,12 @@ use crate::debug_state::DebugState; use crate::kurbo::Vec2; use crate::text::TextStorage; use crate::widget::prelude::*; +use crate::widget::Axis; use crate::{ ArcStr, Color, Data, FontDescriptor, KeyOrValue, LocalizedString, Point, TextAlignment, TextLayout, }; -use tracing::{instrument, trace}; +use tracing::{instrument, trace, warn}; // added padding between the edges of the widget and the text. const LABEL_X_PADDING: f64 = 2.0; @@ -537,6 +538,18 @@ impl Widget for Label { ..Default::default() } } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + _data: &T, + env: &Env, + ) -> f64 { + self.label + .compute_max_intrinsic(axis, ctx, bc, &self.current_text, env) + } } impl Widget for RawLabel { @@ -634,6 +647,35 @@ impl Widget for RawLabel { } self.draw_at(ctx, origin) } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + match axis { + Axis::Horizontal => { + match self.line_break_mode { + LineBreaking::WordWrap => { + // Height is irrelevant for labels. So max preferred/intrinsic width of a label is the size + // it'd take without any word wrapping. + self.line_break_mode = LineBreaking::Clip; + let s = self.layout(ctx, bc, data, env); + self.line_break_mode = LineBreaking::WordWrap; + s.width + } + _ => self.layout(ctx, bc, data, env).width, + } + } + Axis::Vertical => { + warn!("Max intrinsic height of a label is not implemented."); + 0. + } + } + } } impl Default for RawLabel { diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 156c2b4463..9523b54840 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -34,6 +34,7 @@ mod env_scope; mod flex; mod identity_wrapper; mod image; +mod intrinsic_width; mod invalidation; mod label; mod lens_wrap; @@ -79,6 +80,7 @@ pub use either::Either; pub use env_scope::EnvScope; pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; pub use identity_wrapper::IdentityWrapper; +pub use intrinsic_width::IntrinsicWidth; pub use label::{Label, LabelText, LineBreaking, RawLabel}; pub use lens_wrap::LensWrap; pub use list::{List, ListIter}; diff --git a/druid/src/widget/padding.rs b/druid/src/widget/padding.rs index f5680551ca..ca11a38c02 100644 --- a/druid/src/widget/padding.rs +++ b/druid/src/widget/padding.rs @@ -15,7 +15,7 @@ //! A widget that just adds padding during layout. use crate::debug_state::DebugState; -use crate::widget::{prelude::*, WidgetWrapper}; +use crate::widget::{prelude::*, Axis, WidgetWrapper}; use crate::{Data, Insets, KeyOrValue, Point, WidgetPod}; use tracing::{instrument, trace}; @@ -126,4 +126,21 @@ impl> Widget for Padding { ..Default::default() } } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + let inset_size = self.insets.resolve(env).size(); + let child_bc = bc.shrink(inset_size); + let child_max_intrinsic_width = self + .child + .widget_mut() + .compute_max_intrinsic(axis, ctx, &child_bc, data, env); + child_max_intrinsic_width + axis.major(inset_size) + } } diff --git a/druid/src/widget/sized_box.rs b/druid/src/widget/sized_box.rs index 4ba75f09bd..73825accde 100644 --- a/druid/src/widget/sized_box.rs +++ b/druid/src/widget/sized_box.rs @@ -19,6 +19,7 @@ use std::f64::INFINITY; use tracing::{instrument, trace, warn}; use crate::widget::prelude::*; +use crate::widget::Axis; use crate::{Data, KeyOrValue}; /// A widget with predefined size. @@ -218,6 +219,40 @@ impl Widget for SizedBox { ..Default::default() } } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + let kv = match axis { + Axis::Horizontal => self.width.as_ref(), + Axis::Vertical => self.height.as_ref(), + }; + match (self.child.as_mut(), kv) { + (Some(c), Some(v)) => { + let v = v.resolve(env); + if v == f64::INFINITY { + c.compute_max_intrinsic(axis, ctx, bc, data, env) + } else { + v + } + } + (Some(c), None) => c.compute_max_intrinsic(axis, ctx, bc, data, env), + (None, Some(v)) => { + let v = v.resolve(env); + if v == f64::INFINITY { + // If v infinite, we can only warn. + warn!("SizedBox is without a child and its dim is infinite. Either give SizedBox a child or make its dim finite. ") + } + v + } + (None, None) => 0., + } + } } #[cfg(test)] diff --git a/druid/src/widget/widget.rs b/druid/src/widget/widget.rs index a1da95aa1b..2fce2aec2d 100644 --- a/druid/src/widget/widget.rs +++ b/druid/src/widget/widget.rs @@ -17,6 +17,7 @@ use std::ops::{Deref, DerefMut}; use super::prelude::*; use crate::debug_state::DebugState; +use crate::widget::Axis; /// A unique identifier for a single [`Widget`]. /// @@ -222,6 +223,38 @@ pub trait Widget { ..Default::default() } } + + /// Computes max intrinsic/preferred dimension of a widget on the provided axis. + /// + /// Max intrinsic/preferred dimension is the dimension the widget could take, provided infinite + /// constraint on that axis. + /// + /// If axis == Axis::Horizontal, widget is being asked to calculate max intrinsic width. + /// If axis == Axis::Vertical, widget is being asked to calculate max intrinsic height. + /// + /// Box constraints must be honored in intrinsics computation. + /// + /// AspectRatioBox is an example where constraints are honored. If height is finite, max intrinsic + /// width is *height * ratio*. + /// Only when height is infinite, child's max intrinsic width is calculated. + /// + /// Intrinsic is a *could-be* value. It's the value a widget *could* have given infinite constraints. + /// This does not mean the value returned by layout() would be the same. + /// + /// This method **must** return a finite value. + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + match axis { + Axis::Horizontal => self.layout(ctx, bc, data, env).width, + Axis::Vertical => self.layout(ctx, bc, data, env).height, + } + } } impl WidgetId { @@ -291,4 +324,16 @@ impl Widget for Box> { fn debug_state(&self, data: &T) -> DebugState { self.deref().debug_state(data) } + + fn compute_max_intrinsic( + &mut self, + axis: Axis, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &T, + env: &Env, + ) -> f64 { + self.deref_mut() + .compute_max_intrinsic(axis, ctx, bc, data, env) + } }