From 03d367a4091d3749af48bf6447e5ed9bae4a56c4 Mon Sep 17 00:00:00 2001 From: lazypassion <25536767+lazypassion@users.noreply.github.com> Date: Fri, 12 Mar 2021 16:00:29 -0500 Subject: [PATCH 1/8] adding first draft of aspect ratio box --- druid/examples/aspect_ratio_box.rs | 54 ++++++ druid/src/widget/aspect_ratio_box.rs | 258 +++++++++++++++++++++++++++ druid/src/widget/mod.rs | 2 + 3 files changed, 314 insertions(+) create mode 100644 druid/examples/aspect_ratio_box.rs create mode 100644 druid/src/widget/aspect_ratio_box.rs diff --git a/druid/examples/aspect_ratio_box.rs b/druid/examples/aspect_ratio_box.rs new file mode 100644 index 0000000000..3fedd2cd2e --- /dev/null +++ b/druid/examples/aspect_ratio_box.rs @@ -0,0 +1,54 @@ +use druid::{ + widget::{AspectRatioBox, Flex, Label, LineBreaking, MainAxisAlignment, SizedBox}, + AppLauncher, Color, Data, Env, Lens, Widget, WidgetExt, WindowDesc, +}; + +fn main() { + let window = WindowDesc::new(ui()); + let fixed_message = + "Hello there, this is a fixed size box and it will not change no matter what."; + let aspect_ratio_message = + "Hello there, this is a box that maintains it's aspect-ratio as best as possible. Notice text will overflow if box becomes too small."; + AppLauncher::with_window(window) + .use_env_tracing() + .launch(AppState { + fixed_box: fixed_message.to_string(), + aspect_ratio_box: aspect_ratio_message.to_string(), + }) + .unwrap(); +} +#[derive(Clone, Data, Lens, Debug)] +struct AppState { + fixed_box: String, + aspect_ratio_box: String, +} + +fn ui() -> impl Widget { + let fixed_label = Label::new(|data: &String, _env: &Env| data.clone()) + .with_text_color(Color::BLACK) + .with_line_break_mode(LineBreaking::WordWrap) + .center() + .lens(AppState::fixed_box); + let fixed_box = SizedBox::new(fixed_label) + .height(250.) + .width(250.) + .background(Color::WHITE); + + let aspect_ratio_label = Label::new(|data: &String, _env: &Env| data.clone()) + .with_text_color(Color::BLACK) + .with_line_break_mode(LineBreaking::WordWrap) + .center() + .lens(AppState::aspect_ratio_box); + let aspect_ratio_box = AspectRatioBox::new(aspect_ratio_label, 2.0) + .border(Color::BLACK, 1.0) + .background(Color::WHITE); + + Flex::column() + .with_flex_child(fixed_box, 1.0) + // using flex child so that aspect_ratio doesn't get any infinite constraints + // you can use this in `with_child` but there might be some unintended behavior + // the aspect ratio box will work correctly but it might use up all the + // allotted space and make any flex children disappear + .with_flex_child(aspect_ratio_box, 1.0) + .main_axis_alignment(MainAxisAlignment::SpaceEvenly) +} diff --git a/druid/src/widget/aspect_ratio_box.rs b/druid/src/widget/aspect_ratio_box.rs new file mode 100644 index 0000000000..0fde7c3f3e --- /dev/null +++ b/druid/src/widget/aspect_ratio_box.rs @@ -0,0 +1,258 @@ +use druid::widget::prelude::*; +use druid::Data; +use tracing::warn; + +/// A widget that preserves the aspect ratio given to it. +/// +/// If given a child, this widget forces the child to have a width and height that preserves +/// the aspect ratio. +/// +/// If not given a child, The box will try to size itself as large or small as possible +/// to preserve the aspect ratio. +pub struct AspectRatioBox { + inner: Option>>, + ratio: f64, +} + +impl AspectRatioBox { + /// Create container with a child and aspect ratio. + /// + /// If aspect ratio <= 0.0, the ratio will be set to 1.0 + pub fn new(inner: impl Widget + 'static, ratio: f64) -> Self { + Self { + inner: Some(Box::new(inner)), + ratio: AspectRatioBox::::clamp_ratio(ratio), + } + } + + /// Create container without child but with an aspect ratio. + /// + /// If aspect ratio <= 0.0, the ratio will be set to 1.0 + pub fn empty(ratio: f64) -> Self { + Self { + inner: None, + ratio: AspectRatioBox::::clamp_ratio(ratio), + } + } + + /// Set the ratio of the box. + /// + /// The ratio has to be a value between 0 and 1, excluding 0. It will be clamped + /// to those values if they exceed the bounds. If the ratio is 0, then the ratio + /// will become 1. + pub fn set_ratio(&mut self, ratio: f64) { + self.ratio = AspectRatioBox::::clamp_ratio(ratio); + } + + // clamps the ratio between 0.0 and f64::MAX + // if ratio is 0.0 then it will return 1.0 to avoid creating NaN + fn clamp_ratio(mut ratio: f64) -> f64 { + ratio = f64::clamp(ratio, 0.0, f64::MAX); + if ratio == 0.0 { + // should I force the ratio to be 1 in this case? + 1.0 + } else { + ratio + } + } +} + +impl Widget for AspectRatioBox { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + if let Some(ref mut inner) = self.inner { + inner.event(ctx, event, data, env); + } + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + if let Some(ref mut inner) = self.inner { + inner.lifecycle(ctx, event, data, env) + } + } + + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { + if let Some(ref mut inner) = self.inner { + inner.update(ctx, old_data, data, env); + } + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { + bc.debug_check("AspectRatioBox"); + + let bc = if bc.max() == bc.min() { + warn!("Box constraints are tight. Aspect ratio box will not be able to preserve aspect ratio."); + + *bc + } else if bc.max().width == f64::INFINITY && bc.max().height == f64::INFINITY { + warn!("Box constraints are INFINITE. Aspect ratio box won't be able to choose a size because the constraints given by the parent widget are INFINITE."); + + // should I do this or should I just choose a default size + // I size the box with the child's size if there is one + let size = match self.inner.as_mut() { + Some(inner) => inner.layout(ctx, &bc, data, env), + None => Size::new(500., 500.), + }; + + BoxConstraints::new(Size::new(0., 0.), size) + } else { + let (mut box_width, mut box_height) = (bc.max().width, bc.max().height); + + if self.ratio < 1.0 { + if (box_height >= box_width && box_height * self.ratio <= box_width) + || box_width > box_height + { + box_width = box_height * self.ratio; + } else if box_height >= box_width && box_height * self.ratio > box_width { + box_height = box_width / self.ratio; + } else { + // I'm not sure if these sections are necessary or correct + // dbg!("ratio can't be preserved {}", self.ratio); + warn!("The aspect ratio cannot be preserved because one of dimensions is tight and the other dimension is too small: bc.max() = {}, bc.min() = {}", bc.max(), bc.min()); + } + + BoxConstraints::tight(Size::new(box_width, box_height)) + } else if self.ratio > 1.0 { + if box_width > box_height && box_height * self.ratio <= box_width { + box_width = box_height * self.ratio; + } + // this condition might be wrong if box_width and height are equal to each other + // and the aspect ratio is something like 1.00000000000000001, in this case + // the box_height * self.ratio could be equal to box_width + // however 1.00000000000000001 does equal 1.0 + else if (box_width >= box_height && box_height * self.ratio > box_width) + || box_height > box_width + { + box_height = box_width / self.ratio; + } else { + // I'm not sure if these sections are necessary or correct + // dbg!("ratio can't be preserved {}", self.ratio); + warn!("The aspect ratio cannot be preserved because one of dimensions is tight and the other dimension is too small: bc.max() = {}, bc.min() = {}", bc.max(), bc.min()); + } + + BoxConstraints::tight(Size::new(box_width, box_height)) + } + // the aspect ratio is 1:1 which means we want a square + // we take the minimum between the width and height and constrain to that min + else { + let min = box_width.min(box_height); + BoxConstraints::tight(Size::new(min, min)) + } + }; + + let size = match self.inner.as_mut() { + Some(inner) => inner.layout(ctx, &bc, data, env), + None => bc.max(), + }; + + size + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { + if let Some(ref mut inner) = self.inner { + inner.paint(ctx, data, env); + } + } + + fn id(&self) -> Option { + self.inner.as_ref().and_then(|inner| inner.id()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::harness::*; + use crate::widget::Label; + use crate::WidgetExt; + + #[test] + fn tight_constraints() { + let id = WidgetId::next(); + let (width, height) = (400., 400.); + let aspect = AspectRatioBox::<()>::new(Label::new("hello!"), 1.0) + .with_id(id) + .fix_width(width) + .fix_height(height) + .center(); + + let (window_width, window_height) = (600., 600.); + + Harness::create_simple((), aspect, |harness| { + harness.set_initial_size(Size::new(window_width, window_height)); + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id); + assert_eq!(state.layout_rect().size(), Size::new(width, height)); + }); + } + + #[test] + fn infinite_constraints_with_child() { + let id = WidgetId::next(); + let (width, height) = (100., 100.); + let label = Label::new("hello!").fix_width(width).height(height); + let aspect = AspectRatioBox::<()>::new(label, 1.0) + .with_id(id) + .scroll() + .center(); + + let (window_width, window_height) = (600., 600.); + + Harness::create_simple((), aspect, |harness| { + harness.set_initial_size(Size::new(window_width, window_height)); + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id); + assert_eq!(state.layout_rect().size(), Size::new(width, height)); + }); + } + #[test] + fn infinite_constraints_without_child() { + let id = WidgetId::next(); + let aspect = AspectRatioBox::<()>::empty(1.0) + .with_id(id) + .scroll() + .center(); + + let (window_width, window_height) = (600., 600.); + + Harness::create_simple((), aspect, |harness| { + harness.set_initial_size(Size::new(window_width, window_height)); + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id); + assert_eq!(state.layout_rect().size(), Size::new(500., 500.)); + }); + } + + // this test still needs some work + // I am testing for this condition: + // The box constraint on the width's min and max is 300.0. + // The height of the window is 50.0 and width 600.0. + // I'm not sure what size the SizedBox passes in for the height constraint + // but it is most likely 50.0 for max and 0.0 for min. + // The aspect ratio is 2.0 which means the box has to have dimensions (300., 150.) + // however given these constraints it isn't possible. + // should the aspect ratio box maintain aspect ratio anyways or should it clip/overflow? + #[test] + fn tight_constraint_on_width() { + let id = WidgetId::next(); + let label = Label::new("hello!"); + let aspect = AspectRatioBox::<()>::new(label, 2.0) + .with_id(id) + .fix_width(300.) + // wrap in align widget because root widget must fill the window space + .center(); + + let (window_width, window_height) = (600., 50.); + + Harness::create_simple((), aspect, |harness| { + harness.set_initial_size(Size::new(window_width, window_height)); + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id); + dbg!(state.layout_rect().size()); + // assert_eq!(state.layout_rect().size(), Size::new(500., 500.)); + }); + } +} diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 139c9291ce..3c63725c0d 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -20,6 +20,7 @@ mod widget_wrapper; mod added; mod align; +mod aspect_ratio_box; mod button; mod checkbox; mod click; @@ -63,6 +64,7 @@ mod widget_ext; pub use self::image::Image; pub use added::Added; pub use align::Align; +pub use aspect_ratio_box::AspectRatioBox; pub use button::Button; pub use checkbox::Checkbox; pub use click::Click; From 63605ae28e707ba6a94bd0b8fbbd604d7852b5db Mon Sep 17 00:00:00 2001 From: lazypassion <25536767+lazypassion@users.noreply.github.com> Date: Sat, 13 Mar 2021 15:03:10 -0500 Subject: [PATCH 2/8] fixing issue in example with env tracing --- druid/examples/aspect_ratio_box.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/druid/examples/aspect_ratio_box.rs b/druid/examples/aspect_ratio_box.rs index 3fedd2cd2e..ef5006cf6c 100644 --- a/druid/examples/aspect_ratio_box.rs +++ b/druid/examples/aspect_ratio_box.rs @@ -10,7 +10,6 @@ fn main() { let aspect_ratio_message = "Hello there, this is a box that maintains it's aspect-ratio as best as possible. Notice text will overflow if box becomes too small."; AppLauncher::with_window(window) - .use_env_tracing() .launch(AppState { fixed_box: fixed_message.to_string(), aspect_ratio_box: aspect_ratio_message.to_string(), From 7174d144378233ee119777197c158bf280152849 Mon Sep 17 00:00:00 2001 From: lazypassion <25536767+lazypassion@users.noreply.github.com> Date: Sat, 13 Mar 2021 15:12:59 -0500 Subject: [PATCH 3/8] adding instrument macro to widget impl --- druid/src/widget/aspect_ratio_box.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/druid/src/widget/aspect_ratio_box.rs b/druid/src/widget/aspect_ratio_box.rs index 0fde7c3f3e..2b84176243 100644 --- a/druid/src/widget/aspect_ratio_box.rs +++ b/druid/src/widget/aspect_ratio_box.rs @@ -1,6 +1,6 @@ use druid::widget::prelude::*; use druid::Data; -use tracing::warn; +use tracing::{instrument, warn}; /// A widget that preserves the aspect ratio given to it. /// @@ -58,24 +58,44 @@ impl AspectRatioBox { } impl Widget for AspectRatioBox { + #[instrument( + name = "AspectRatioBox", + level = "trace", + skip(self, ctx, event, data, env) + )] fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { if let Some(ref mut inner) = self.inner { inner.event(ctx, event, data, env); } } + #[instrument( + name = "AspectRatioBox", + level = "trace", + skip(self, ctx, event, data, env) + )] fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { if let Some(ref mut inner) = self.inner { inner.lifecycle(ctx, event, data, env) } } + #[instrument( + name = "AspectRatioBox", + level = "trace", + skip(self, ctx, old_data, data, env) + )] fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { if let Some(ref mut inner) = self.inner { inner.update(ctx, old_data, data, env); } } + #[instrument( + name = "AspectRatioBox", + level = "trace", + skip(self, ctx, bc, data, env) + )] fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { bc.debug_check("AspectRatioBox"); @@ -147,6 +167,7 @@ impl Widget for AspectRatioBox { size } + #[instrument(name = "AspectRatioBox", level = "trace", skip(self, ctx, data, env))] fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { if let Some(ref mut inner) = self.inner { inner.paint(ctx, data, env); From 2322c0be1ce811b56462a1b802aa3833990fd958 Mon Sep 17 00:00:00 2001 From: lazypassion <25536767+lazypassion@users.noreply.github.com> Date: Fri, 19 Mar 2021 18:49:11 -0400 Subject: [PATCH 4/8] updating generate aspect ratio logic - move tests to layout --- druid/src/tests/layout_tests.rs | 78 ++++++++ druid/src/widget/aspect_ratio_box.rs | 275 ++++++++------------------- 2 files changed, 158 insertions(+), 195 deletions(-) diff --git a/druid/src/tests/layout_tests.rs b/druid/src/tests/layout_tests.rs index 6e3fe4b4a9..75596a2363 100644 --- a/druid/src/tests/layout_tests.rs +++ b/druid/src/tests/layout_tests.rs @@ -186,3 +186,81 @@ fn flex_paint_rect_overflow() { assert_eq!(state.paint_rect().size(), expected_paint_rect.size()); }) } + +use crate::tests::harness::*; +use crate::widget::AspectRatioBox; +use crate::widget::Label; +use crate::WidgetExt; + +#[test] +fn tight_constraints() { + let id = WidgetId::next(); + let (width, height) = (400., 400.); + let aspect = AspectRatioBox::<()>::new(Label::new("hello!"), 1.0) + .with_id(id) + .fix_width(width) + .fix_height(height) + .center(); + + let (window_width, window_height) = (600., 600.); + + Harness::create_simple((), aspect, |harness| { + harness.set_initial_size(Size::new(window_width, window_height)); + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id); + assert_eq!(state.layout_rect().size(), Size::new(width, height)); + }); +} + +#[test] +fn infinite_constraints_with_child() { + let id = WidgetId::next(); + let (width, height) = (100., 100.); + let label = Label::new("hello!").fix_width(width).height(height); + let aspect = AspectRatioBox::<()>::new(label, 1.0) + .with_id(id) + .scroll() + .center(); + + let (window_width, window_height) = (600., 600.); + + Harness::create_simple((), aspect, |harness| { + harness.set_initial_size(Size::new(window_width, window_height)); + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id); + assert_eq!(state.layout_rect().size(), Size::new(width, height)); + }); +} + +// this test still needs some work +// I am testing for this condition: +// The box constraint on the width's min and max is 300.0. +// The height of the window is 50.0 and width 600.0. +// I'm not sure what size the SizedBox passes in for the height constraint +// but it is most likely 50.0 for max and 0.0 for min. +// The aspect ratio is 2.0 which means the box has to have dimensions (300., 150.) +// however given these constraints it isn't possible. +// should the aspect ratio box maintain aspect ratio anyways or should it clip/overflow? +#[test] +fn tight_constraint_on_width() { + let id = WidgetId::next(); + let label = Label::new("hello!"); + let aspect = AspectRatioBox::<()>::new(label, 2.0) + .with_id(id) + .fix_width(300.) + // wrap in align widget because root widget must fill the window space + .center(); + + let (window_width, window_height) = (600., 50.); + + Harness::create_simple((), aspect, |harness| { + harness.set_initial_size(Size::new(window_width, window_height)); + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id); + dbg!(state.layout_rect().size()); + // assert_eq!(state.layout_rect().size(), Size::new(500., 500.)); + }); +} diff --git a/druid/src/widget/aspect_ratio_box.rs b/druid/src/widget/aspect_ratio_box.rs index 2b84176243..0b78ce698b 100644 --- a/druid/src/widget/aspect_ratio_box.rs +++ b/druid/src/widget/aspect_ratio_box.rs @@ -1,3 +1,17 @@ +// Copyright 2021 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 druid::widget::prelude::*; use druid::Data; use tracing::{instrument, warn}; @@ -10,50 +24,79 @@ use tracing::{instrument, warn}; /// If not given a child, The box will try to size itself as large or small as possible /// to preserve the aspect ratio. pub struct AspectRatioBox { - inner: Option>>, + inner: Box>, ratio: f64, } impl AspectRatioBox { /// Create container with a child and aspect ratio. /// - /// If aspect ratio <= 0.0, the ratio will be set to 1.0 - pub fn new(inner: impl Widget + 'static, ratio: f64) -> Self { - Self { - inner: Some(Box::new(inner)), - ratio: AspectRatioBox::::clamp_ratio(ratio), - } - } - - /// Create container without child but with an aspect ratio. + /// The aspect ratio is defined as width / height. /// /// If aspect ratio <= 0.0, the ratio will be set to 1.0 - pub fn empty(ratio: f64) -> Self { + pub fn new(inner: impl Widget + 'static, ratio: f64) -> Self { Self { - inner: None, - ratio: AspectRatioBox::::clamp_ratio(ratio), + inner: Box::new(inner), + ratio: clamp_ratio(ratio), } } /// Set the ratio of the box. /// - /// The ratio has to be a value between 0 and 1, excluding 0. It will be clamped + /// The ratio has to be a value between 0 and f64::MAX, excluding 0. It will be clamped /// to those values if they exceed the bounds. If the ratio is 0, then the ratio /// will become 1. pub fn set_ratio(&mut self, ratio: f64) { - self.ratio = AspectRatioBox::::clamp_ratio(ratio); + self.ratio = clamp_ratio(ratio); } - // clamps the ratio between 0.0 and f64::MAX - // if ratio is 0.0 then it will return 1.0 to avoid creating NaN - fn clamp_ratio(mut ratio: f64) -> f64 { - ratio = f64::clamp(ratio, 0.0, f64::MAX); - if ratio == 0.0 { - // should I force the ratio to be 1 in this case? - 1.0 + /// Generate `BoxConstraints` that fit within the provided `BoxConstraints`. + /// + /// If the generated constraints do not fit then they are constrained to the + /// provided `BoxConstraints`. + fn generate_constraints(&self, bc: &BoxConstraints) -> BoxConstraints { + let (mut new_width, mut new_height) = (bc.max().width, bc.max().height); + + if new_width == f64::INFINITY { + new_width = new_height * self.ratio; } else { - ratio + new_height = new_width / self.ratio; + } + + if new_width > bc.max().width { + new_width = bc.max().width; + new_height = new_width / self.ratio; + } + + if new_height > bc.max().height { + new_height = bc.max().height; + new_width = new_height * self.ratio; + } + + if new_width < bc.min().width { + new_width = bc.min().width; + new_height = new_width / self.ratio; } + + if new_height < bc.min().height { + new_height = bc.min().height; + new_width = new_height * self.ratio; + } + + BoxConstraints::tight(bc.constrain(Size::new(new_width, new_height))) + } +} + +/// Clamps the ratio between 0.0 and f64::MAX +/// If ratio is 0.0 then it will return 1.0 to avoid creating NaN +fn clamp_ratio(mut ratio: f64) -> f64 { + ratio = f64::clamp(ratio, 0.0, f64::MAX); + + if ratio == 0.0 { + warn!("Provided ratio was <= 0.0."); + 1.0 + } else { + ratio } } @@ -64,9 +107,7 @@ impl Widget for AspectRatioBox { skip(self, ctx, event, data, env) )] fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { - if let Some(ref mut inner) = self.inner { - inner.event(ctx, event, data, env); - } + self.inner.event(ctx, event, data, env); } #[instrument( @@ -75,9 +116,7 @@ impl Widget for AspectRatioBox { skip(self, ctx, event, data, env) )] fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { - if let Some(ref mut inner) = self.inner { - inner.lifecycle(ctx, event, data, env) - } + self.inner.lifecycle(ctx, event, data, env) } #[instrument( @@ -86,9 +125,7 @@ impl Widget for AspectRatioBox { skip(self, ctx, old_data, data, env) )] fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { - if let Some(ref mut inner) = self.inner { - inner.update(ctx, old_data, data, env); - } + self.inner.update(ctx, old_data, data, env); } #[instrument( @@ -99,181 +136,29 @@ impl Widget for AspectRatioBox { fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { bc.debug_check("AspectRatioBox"); - let bc = if bc.max() == bc.min() { + if bc.max() == bc.min() { warn!("Box constraints are tight. Aspect ratio box will not be able to preserve aspect ratio."); - *bc - } else if bc.max().width == f64::INFINITY && bc.max().height == f64::INFINITY { - warn!("Box constraints are INFINITE. Aspect ratio box won't be able to choose a size because the constraints given by the parent widget are INFINITE."); - - // should I do this or should I just choose a default size - // I size the box with the child's size if there is one - let size = match self.inner.as_mut() { - Some(inner) => inner.layout(ctx, &bc, data, env), - None => Size::new(500., 500.), - }; - - BoxConstraints::new(Size::new(0., 0.), size) - } else { - let (mut box_width, mut box_height) = (bc.max().width, bc.max().height); - - if self.ratio < 1.0 { - if (box_height >= box_width && box_height * self.ratio <= box_width) - || box_width > box_height - { - box_width = box_height * self.ratio; - } else if box_height >= box_width && box_height * self.ratio > box_width { - box_height = box_width / self.ratio; - } else { - // I'm not sure if these sections are necessary or correct - // dbg!("ratio can't be preserved {}", self.ratio); - warn!("The aspect ratio cannot be preserved because one of dimensions is tight and the other dimension is too small: bc.max() = {}, bc.min() = {}", bc.max(), bc.min()); - } + return self.inner.layout(ctx, &bc, data, env); + } - BoxConstraints::tight(Size::new(box_width, box_height)) - } else if self.ratio > 1.0 { - if box_width > box_height && box_height * self.ratio <= box_width { - box_width = box_height * self.ratio; - } - // this condition might be wrong if box_width and height are equal to each other - // and the aspect ratio is something like 1.00000000000000001, in this case - // the box_height * self.ratio could be equal to box_width - // however 1.00000000000000001 does equal 1.0 - else if (box_width >= box_height && box_height * self.ratio > box_width) - || box_height > box_width - { - box_height = box_width / self.ratio; - } else { - // I'm not sure if these sections are necessary or correct - // dbg!("ratio can't be preserved {}", self.ratio); - warn!("The aspect ratio cannot be preserved because one of dimensions is tight and the other dimension is too small: bc.max() = {}, bc.min() = {}", bc.max(), bc.min()); - } + if bc.max().width == f64::INFINITY && bc.max().height == f64::INFINITY { + warn!("Box constraints are INFINITE. Aspect ratio box won't be able to choose a size because the constraints given by the parent widget are INFINITE."); - BoxConstraints::tight(Size::new(box_width, box_height)) - } - // the aspect ratio is 1:1 which means we want a square - // we take the minimum between the width and height and constrain to that min - else { - let min = box_width.min(box_height); - BoxConstraints::tight(Size::new(min, min)) - } - }; + return self.inner.layout(ctx, &bc, data, env); + } - let size = match self.inner.as_mut() { - Some(inner) => inner.layout(ctx, &bc, data, env), - None => bc.max(), - }; + let bc = self.generate_constraints(bc); - size + self.inner.layout(ctx, &bc, data, env) } #[instrument(name = "AspectRatioBox", level = "trace", skip(self, ctx, data, env))] fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { - if let Some(ref mut inner) = self.inner { - inner.paint(ctx, data, env); - } + self.inner.paint(ctx, data, env); } fn id(&self) -> Option { - self.inner.as_ref().and_then(|inner| inner.id()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::harness::*; - use crate::widget::Label; - use crate::WidgetExt; - - #[test] - fn tight_constraints() { - let id = WidgetId::next(); - let (width, height) = (400., 400.); - let aspect = AspectRatioBox::<()>::new(Label::new("hello!"), 1.0) - .with_id(id) - .fix_width(width) - .fix_height(height) - .center(); - - let (window_width, window_height) = (600., 600.); - - Harness::create_simple((), aspect, |harness| { - harness.set_initial_size(Size::new(window_width, window_height)); - harness.send_initial_events(); - harness.just_layout(); - let state = harness.get_state(id); - assert_eq!(state.layout_rect().size(), Size::new(width, height)); - }); - } - - #[test] - fn infinite_constraints_with_child() { - let id = WidgetId::next(); - let (width, height) = (100., 100.); - let label = Label::new("hello!").fix_width(width).height(height); - let aspect = AspectRatioBox::<()>::new(label, 1.0) - .with_id(id) - .scroll() - .center(); - - let (window_width, window_height) = (600., 600.); - - Harness::create_simple((), aspect, |harness| { - harness.set_initial_size(Size::new(window_width, window_height)); - harness.send_initial_events(); - harness.just_layout(); - let state = harness.get_state(id); - assert_eq!(state.layout_rect().size(), Size::new(width, height)); - }); - } - #[test] - fn infinite_constraints_without_child() { - let id = WidgetId::next(); - let aspect = AspectRatioBox::<()>::empty(1.0) - .with_id(id) - .scroll() - .center(); - - let (window_width, window_height) = (600., 600.); - - Harness::create_simple((), aspect, |harness| { - harness.set_initial_size(Size::new(window_width, window_height)); - harness.send_initial_events(); - harness.just_layout(); - let state = harness.get_state(id); - assert_eq!(state.layout_rect().size(), Size::new(500., 500.)); - }); - } - - // this test still needs some work - // I am testing for this condition: - // The box constraint on the width's min and max is 300.0. - // The height of the window is 50.0 and width 600.0. - // I'm not sure what size the SizedBox passes in for the height constraint - // but it is most likely 50.0 for max and 0.0 for min. - // The aspect ratio is 2.0 which means the box has to have dimensions (300., 150.) - // however given these constraints it isn't possible. - // should the aspect ratio box maintain aspect ratio anyways or should it clip/overflow? - #[test] - fn tight_constraint_on_width() { - let id = WidgetId::next(); - let label = Label::new("hello!"); - let aspect = AspectRatioBox::<()>::new(label, 2.0) - .with_id(id) - .fix_width(300.) - // wrap in align widget because root widget must fill the window space - .center(); - - let (window_width, window_height) = (600., 50.); - - Harness::create_simple((), aspect, |harness| { - harness.set_initial_size(Size::new(window_width, window_height)); - harness.send_initial_events(); - harness.just_layout(); - let state = harness.get_state(id); - dbg!(state.layout_rect().size()); - // assert_eq!(state.layout_rect().size(), Size::new(500., 500.)); - }); + self.inner.id() } } From 4c3fd14cf9aacdcec329cd42a10afb5a1a41e0b9 Mon Sep 17 00:00:00 2001 From: lazypassion <25536767+lazypassion@users.noreply.github.com> Date: Fri, 19 Mar 2021 19:01:08 -0400 Subject: [PATCH 5/8] updating layout tests for aspect ratio --- druid/src/tests/layout_tests.rs | 39 ++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/druid/src/tests/layout_tests.rs b/druid/src/tests/layout_tests.rs index 75596a2363..16d2665eca 100644 --- a/druid/src/tests/layout_tests.rs +++ b/druid/src/tests/layout_tests.rs @@ -193,7 +193,7 @@ use crate::widget::Label; use crate::WidgetExt; #[test] -fn tight_constraints() { +fn aspect_ratio_tight_constraints() { let id = WidgetId::next(); let (width, height) = (400., 400.); let aspect = AspectRatioBox::<()>::new(Label::new("hello!"), 1.0) @@ -214,7 +214,7 @@ fn tight_constraints() { } #[test] -fn infinite_constraints_with_child() { +fn aspect_ratio_infinite_constraints() { let id = WidgetId::next(); let (width, height) = (100., 100.); let label = Label::new("hello!").fix_width(width).height(height); @@ -234,23 +234,13 @@ fn infinite_constraints_with_child() { }); } -// this test still needs some work -// I am testing for this condition: -// The box constraint on the width's min and max is 300.0. -// The height of the window is 50.0 and width 600.0. -// I'm not sure what size the SizedBox passes in for the height constraint -// but it is most likely 50.0 for max and 0.0 for min. -// The aspect ratio is 2.0 which means the box has to have dimensions (300., 150.) -// however given these constraints it isn't possible. -// should the aspect ratio box maintain aspect ratio anyways or should it clip/overflow? #[test] -fn tight_constraint_on_width() { +fn aspect_ratio_tight_constraint_on_width() { let id = WidgetId::next(); let label = Label::new("hello!"); let aspect = AspectRatioBox::<()>::new(label, 2.0) .with_id(id) .fix_width(300.) - // wrap in align widget because root widget must fill the window space .center(); let (window_width, window_height) = (600., 50.); @@ -260,7 +250,26 @@ fn tight_constraint_on_width() { harness.send_initial_events(); harness.just_layout(); let state = harness.get_state(id); - dbg!(state.layout_rect().size()); - // assert_eq!(state.layout_rect().size(), Size::new(500., 500.)); + assert_eq!(state.layout_rect().size(), Size::new(300., 50.)); + }); +} + +#[test] +fn aspect_ratio() { + let id = WidgetId::next(); + let label = Label::new("hello!"); + let aspect = AspectRatioBox::<()>::new(label, 2.0) + .with_id(id) + .center() + .center(); + + let (window_width, window_height) = (1000., 1000.); + + Harness::create_simple((), aspect, |harness| { + harness.set_initial_size(Size::new(window_width, window_height)); + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id); + assert_eq!(state.layout_rect().size(), Size::new(1000., 500.)); }); } From 0f2e9effdf1e1c3d174a4f55e16af83faaa4910c Mon Sep 17 00:00:00 2001 From: lazypassion <25536767+lazypassion@users.noreply.github.com> Date: Fri, 19 Mar 2021 19:22:06 -0400 Subject: [PATCH 6/8] updating changelog with aspect ratio box info --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a76f38595..03405233df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ You can find its changes [documented below](#070---2021-01-01). - International text input support (IME) on macOS. ### Added +- Add `AspectRatioBox` widget ([#1645] by [@arthmis]) - Add `scroll()` method in WidgetExt ([#1600] by [@totsteps]) - `write!` for `RichTextBuilder` ([#1596] by [@Maan2003]) - Sub windows: Allow opening windows that share state with arbitrary parts of the widget hierarchy ([#1254] by [@rjwittams]) @@ -390,6 +391,7 @@ Last release without a changelog :( ## 0.1.1 - 2018-11-02 ## 0.1.0 - 2018-11-02 +[@arthmis]: https://github.com/arthmis [@futurepaul]: https://github.com/futurepaul [@finnerale]: https://github.com/finnerale [@totsteps]: https://github.com/totsteps @@ -645,6 +647,7 @@ Last release without a changelog :( [#1636]: https://github.com/linebender/druid/pull/1636 [#1640]: https://github.com/linebender/druid/pull/1640 [#1641]: https://github.com/linebender/druid/pull/1641 +[#1645]: https://github.com/linebender/druid/pull/1645 [#1647]: https://github.com/linebender/druid/pull/1647 [Unreleased]: https://github.com/linebender/druid/compare/v0.7.0...master From f2622293c9cfff993cc90a51114074b777916af9 Mon Sep 17 00:00:00 2001 From: lazypassion <25536767+lazypassion@users.noreply.github.com> Date: Wed, 12 May 2021 16:28:55 -0400 Subject: [PATCH 7/8] pushing changes to layout with addition of aspect_ratio, - still some ci issues to fix --- druid/examples/aspect_ratio_box.rs | 53 ------------------------------ druid/examples/layout.rs | 13 +++++++- 2 files changed, 12 insertions(+), 54 deletions(-) delete mode 100644 druid/examples/aspect_ratio_box.rs diff --git a/druid/examples/aspect_ratio_box.rs b/druid/examples/aspect_ratio_box.rs deleted file mode 100644 index ef5006cf6c..0000000000 --- a/druid/examples/aspect_ratio_box.rs +++ /dev/null @@ -1,53 +0,0 @@ -use druid::{ - widget::{AspectRatioBox, Flex, Label, LineBreaking, MainAxisAlignment, SizedBox}, - AppLauncher, Color, Data, Env, Lens, Widget, WidgetExt, WindowDesc, -}; - -fn main() { - let window = WindowDesc::new(ui()); - let fixed_message = - "Hello there, this is a fixed size box and it will not change no matter what."; - let aspect_ratio_message = - "Hello there, this is a box that maintains it's aspect-ratio as best as possible. Notice text will overflow if box becomes too small."; - AppLauncher::with_window(window) - .launch(AppState { - fixed_box: fixed_message.to_string(), - aspect_ratio_box: aspect_ratio_message.to_string(), - }) - .unwrap(); -} -#[derive(Clone, Data, Lens, Debug)] -struct AppState { - fixed_box: String, - aspect_ratio_box: String, -} - -fn ui() -> impl Widget { - let fixed_label = Label::new(|data: &String, _env: &Env| data.clone()) - .with_text_color(Color::BLACK) - .with_line_break_mode(LineBreaking::WordWrap) - .center() - .lens(AppState::fixed_box); - let fixed_box = SizedBox::new(fixed_label) - .height(250.) - .width(250.) - .background(Color::WHITE); - - let aspect_ratio_label = Label::new(|data: &String, _env: &Env| data.clone()) - .with_text_color(Color::BLACK) - .with_line_break_mode(LineBreaking::WordWrap) - .center() - .lens(AppState::aspect_ratio_box); - let aspect_ratio_box = AspectRatioBox::new(aspect_ratio_label, 2.0) - .border(Color::BLACK, 1.0) - .background(Color::WHITE); - - Flex::column() - .with_flex_child(fixed_box, 1.0) - // using flex child so that aspect_ratio doesn't get any infinite constraints - // you can use this in `with_child` but there might be some unintended behavior - // the aspect ratio box will work correctly but it might use up all the - // allotted space and make any flex children disappear - .with_flex_child(aspect_ratio_box, 1.0) - .main_axis_alignment(MainAxisAlignment::SpaceEvenly) -} diff --git a/druid/examples/layout.rs b/druid/examples/layout.rs index 3bff287a54..679c1703ae 100644 --- a/druid/examples/layout.rs +++ b/druid/examples/layout.rs @@ -15,7 +15,7 @@ //! This example shows how to construct a basic layout, //! using columns, rows, and loops, for repeated Widgets. -use druid::widget::{Button, Flex, Label}; +use druid::widget::{AspectRatioBox, Button, Flex, Label, LineBreaking}; use druid::{AppLauncher, Color, Widget, WidgetExt, WindowDesc}; fn build_app() -> impl Widget { @@ -60,6 +60,17 @@ fn build_app() -> impl Widget { weight, ); } + + // aspect ratio box + let aspect_ratio_label = Label::new("Hello there, this box it's aspect-ratio. Notice text will overflow if box becomes too small.") + .with_text_color(Color::BLACK) + .with_line_break_mode(LineBreaking::WordWrap) + .center(); + let aspect_ratio_box = AspectRatioBox::new(aspect_ratio_label, 4.0) + .border(Color::BLACK, 1.0) + .background(Color::WHITE); + col.add_flex_child(aspect_ratio_box.center(), 1.0); + // This method asks druid to draw colored rectangles around our widgets, // so we can visually inspect their layout rectangles. col.debug_paint_layout() From a513db656d4e53bc61a331eff758c360e2c76759 Mon Sep 17 00:00:00 2001 From: lazypassion <25536767+lazypassion@users.noreply.github.com> Date: Wed, 12 May 2021 16:36:25 -0400 Subject: [PATCH 8/8] fixing message in layout example --- druid/examples/layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/druid/examples/layout.rs b/druid/examples/layout.rs index 679c1703ae..e656ac49eb 100644 --- a/druid/examples/layout.rs +++ b/druid/examples/layout.rs @@ -62,7 +62,7 @@ fn build_app() -> impl Widget { } // aspect ratio box - let aspect_ratio_label = Label::new("Hello there, this box it's aspect-ratio. Notice text will overflow if box becomes too small.") + let aspect_ratio_label = Label::new("This is an aspect-ratio box. Notice how the text will overflow if the box becomes too small.") .with_text_color(Color::BLACK) .with_line_break_mode(LineBreaking::WordWrap) .center();