Skip to content

Commit

Permalink
Adding aspect-ratio widget (#1645)
Browse files Browse the repository at this point in the history
- adding aspect-ratio box widget, which sizes its child to maintain a given aspect ratio
  • Loading branch information
arthmis authored May 12, 2021
1 parent 6f070f1 commit c1188ee
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ You can find its changes [documented below](#070---2021-01-01).
- New `TextBox` widget with IME integration ([#1636] by [@cmyr])
- `Notification`s can be submitted while handling other `Notification`s ([#1640] by [@cmyr])
- Added ListIter implementations for OrdMap ([#1641] by [@Lejero])
- Add `AspectRatioBox` widget ([#1645] by [@arthmis])
- `Padding` can now use `Key<Insets>` ([#1662] by [@cmyr])
- `LifeCycle::DisabledChanged`, `InternalLifeCycle::RouteDisabledChanged` and the `set_disabled()` and `is_disabled()`
context-methods to implement disabled ([#1632] by [@xarvic])
Expand Down Expand Up @@ -425,6 +426,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
Expand Down Expand Up @@ -691,6 +693,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
[#1654]: https://github.com/linebender/druid/pull/1654
[#1660]: https://github.com/linebender/druid/pull/1660
Expand Down
13 changes: 12 additions & 1 deletion druid/examples/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32> {
Expand Down Expand Up @@ -60,6 +60,17 @@ fn build_app() -> impl Widget<u32> {
weight,
);
}

// aspect ratio box
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();
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()
Expand Down
87 changes: 87 additions & 0 deletions druid/src/tests/layout_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,90 @@ 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 aspect_ratio_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 aspect_ratio_infinite_constraints() {
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 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.)
.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);
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.));
});
}
164 changes: 164 additions & 0 deletions druid/src/widget/aspect_ratio_box.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// 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};

/// 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<T> {
inner: Box<dyn Widget<T>>,
ratio: f64,
}

impl<T> AspectRatioBox<T> {
/// Create container with a child and 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 new(inner: impl Widget<T> + 'static, ratio: f64) -> Self {
Self {
inner: Box::new(inner),
ratio: clamp_ratio(ratio),
}
}

/// Set the ratio of the box.
///
/// 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 = clamp_ratio(ratio);
}

/// 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 {
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
}
}

impl<T: Data> Widget<T> for AspectRatioBox<T> {
#[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) {
self.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) {
self.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) {
self.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");

if bc.max() == bc.min() {
warn!("Box constraints are tight. Aspect ratio box will not be able to preserve aspect ratio.");

return self.inner.layout(ctx, &bc, data, env);
}

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.");

return self.inner.layout(ctx, &bc, data, env);
}

let bc = self.generate_constraints(bc);

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) {
self.inner.paint(ctx, data, env);
}

fn id(&self) -> Option<WidgetId> {
self.inner.id()
}
}
2 changes: 2 additions & 0 deletions druid/src/widget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod widget_wrapper;

mod added;
mod align;
mod aspect_ratio_box;
mod button;
mod checkbox;
mod click;
Expand Down Expand Up @@ -65,6 +66,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;
Expand Down

0 comments on commit c1188ee

Please sign in to comment.