mirror of
https://github.com/danbulant/cushy
synced 2026-06-15 12:31:11 +00:00
parent
c4200e6009
commit
0fd7c8fd5c
8 changed files with 350 additions and 17 deletions
65
examples/wrap.rs
Normal file
65
examples/wrap.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use gooey::styles::components::{LineHeight, TextSize};
|
||||
use gooey::value::Dynamic;
|
||||
use gooey::widget::{Children, MakeWidget};
|
||||
use gooey::widgets::wrap::{VerticalAlign, WrapAlign};
|
||||
use gooey::Run;
|
||||
use kludgine::figures::units::Lp;
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
const EXPLANATION: &str = "This example demonstrates the Wrap widget. Each word shown here is an individual Label widget that is being positioned by the Wrap widget.";
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let mut rng = thread_rng();
|
||||
let words = EXPLANATION
|
||||
.split_ascii_whitespace()
|
||||
.map(|word| {
|
||||
let text_size = Lp::points(rng.gen_range(14..48));
|
||||
word.with(&TextSize, text_size).with(&LineHeight, text_size)
|
||||
})
|
||||
.collect::<Children>();
|
||||
|
||||
let align = Dynamic::<WrapAlign>::default();
|
||||
let vertical_align = Dynamic::<VerticalAlign>::default();
|
||||
|
||||
let editors = "Settings"
|
||||
.h3()
|
||||
.and(
|
||||
"Wrap Align"
|
||||
.h5()
|
||||
.and(align.new_radio(WrapAlign::Start, "Start"))
|
||||
.and(align.new_radio(WrapAlign::End, "End"))
|
||||
.and(align.new_radio(WrapAlign::Center, "Center"))
|
||||
.and(align.new_radio(WrapAlign::SpaceAround, "Space Around"))
|
||||
.and(align.new_radio(WrapAlign::SpaceEvenly, "Space Evenly"))
|
||||
.and(align.new_radio(WrapAlign::SpaceBetween, "Space Between"))
|
||||
.into_rows()
|
||||
.contain(),
|
||||
)
|
||||
.and(
|
||||
"Vertical Align"
|
||||
.h5()
|
||||
.and(vertical_align.new_radio(VerticalAlign::Top, "Top"))
|
||||
.and(vertical_align.new_radio(VerticalAlign::Middle, "Middle"))
|
||||
.and(vertical_align.new_radio(VerticalAlign::Bottom, "Bottom"))
|
||||
.into_rows()
|
||||
.contain(),
|
||||
)
|
||||
.into_rows();
|
||||
|
||||
let preview = "Preview"
|
||||
.h3()
|
||||
.and(
|
||||
words
|
||||
.wrap()
|
||||
.align(align)
|
||||
.vertical_align(vertical_align)
|
||||
.expand_horizontally()
|
||||
.contain()
|
||||
.pad()
|
||||
.expand(),
|
||||
)
|
||||
.into_rows()
|
||||
.expand();
|
||||
|
||||
editors.and(preview).into_columns().pad().run()
|
||||
}
|
||||
13
src/value.rs
13
src/value.rs
|
|
@ -1647,7 +1647,7 @@ impl<T> Value<T> {
|
|||
|
||||
/// Returns a clone of the currently stored value.
|
||||
///
|
||||
/// If `self` is a dynamic, `context` will be invalidated when the value is
|
||||
/// If `self` is a dynamic, `context` will be refreshed when the value is
|
||||
/// updated.
|
||||
pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
|
|
@ -1656,6 +1656,17 @@ impl<T> Value<T> {
|
|||
self.map_tracking_redraw(context, Clone::clone)
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently stored value.
|
||||
///
|
||||
/// If `self` is a dynamic, `context` will be invalidated when the value is
|
||||
/// updated.
|
||||
pub fn get_tracking_invalidate(&self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.map_tracking_invalidate(context, Clone::clone)
|
||||
}
|
||||
|
||||
/// Returns the current generation of the data stored, if the contained
|
||||
/// value is [`Dynamic`].
|
||||
pub fn generation(&self) -> Option<Generation> {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ use crate::widgets::checkbox::{Checkable, CheckboxState};
|
|||
use crate::widgets::layers::{OverlayLayer, Tooltipped};
|
||||
use crate::widgets::{
|
||||
Align, Button, Checkbox, Collapse, Container, Expand, Layers, Resize, Scroll, Space, Stack,
|
||||
Style, Themed, ThemedMode, Validated,
|
||||
Style, Themed, ThemedMode, Validated, Wrap,
|
||||
};
|
||||
use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
|
||||
use crate::{ConstraintLimit, Run};
|
||||
|
|
@ -1763,6 +1763,13 @@ impl Children {
|
|||
Layers::new(self)
|
||||
}
|
||||
|
||||
/// Returns a [`Wrap`] that lays the children out horizontally, wrapping
|
||||
/// into additional rows as needed.
|
||||
#[must_use]
|
||||
pub fn wrap(self) -> Wrap {
|
||||
Wrap::new(self)
|
||||
}
|
||||
|
||||
/// Synchronizes this list of children with another collection.
|
||||
///
|
||||
/// This function updates `collection` by calling `change_fn` for each
|
||||
|
|
@ -1829,6 +1836,13 @@ impl Dynamic<Children> {
|
|||
pub fn into_layers(self) -> Layers {
|
||||
Layers::new(self)
|
||||
}
|
||||
|
||||
/// Returns a [`Wrap`] that lays the children out horizontally, wrapping
|
||||
/// into additional rows as needed.
|
||||
#[must_use]
|
||||
pub fn wrap(self) -> Wrap {
|
||||
Wrap::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> FromIterator<W> for Children
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ mod switcher;
|
|||
mod themed;
|
||||
mod tilemap;
|
||||
mod validated;
|
||||
pub mod wrap;
|
||||
|
||||
pub use align::Align;
|
||||
pub use button::Button;
|
||||
|
|
@ -56,3 +57,4 @@ pub use switcher::Switcher;
|
|||
pub use themed::Themed;
|
||||
pub use tilemap::TileMap;
|
||||
pub use validated::Validated;
|
||||
pub use wrap::Wrap;
|
||||
|
|
|
|||
|
|
@ -168,11 +168,6 @@ impl Container {
|
|||
.map(|dim| dim.into_px(context.gfx.scale()))
|
||||
}
|
||||
|
||||
fn effective_shadow(&self, context: &WidgetContext<'_, '_>) -> ContainerShadow {
|
||||
self.shadow.invalidate_when_changed(context);
|
||||
self.shadow.get()
|
||||
}
|
||||
|
||||
fn effective_background_color(&mut self, context: &WidgetContext<'_, '_>) -> kludgine::Color {
|
||||
let background = match self.background.get() {
|
||||
ContainerBackground::Color(color) => EffectiveBackground::Color(color),
|
||||
|
|
@ -224,7 +219,10 @@ impl Widget for Container {
|
|||
let background = self.effective_background_color(context);
|
||||
let background = background.with_alpha_f32(background.alpha_f32() * *opacity);
|
||||
if background.alpha() > 0 {
|
||||
let shadow = self.effective_shadow(context).into_px(context.gfx.scale());
|
||||
let shadow = self
|
||||
.shadow
|
||||
.get_tracking_invalidate(context)
|
||||
.into_px(context.gfx.scale());
|
||||
|
||||
let child_shadow_offset = shadow.offset.min(Point::ZERO).abs();
|
||||
let child_size = context.gfx.region().size - shadow.spread * 2 - shadow.offset.abs();
|
||||
|
|
@ -282,7 +280,10 @@ impl Widget for Container {
|
|||
.max(corner_radii.bottom_left / std::f32::consts::PI);
|
||||
let padding_amount = padding.size();
|
||||
|
||||
let shadow = self.effective_shadow(context).into_px(context.gfx.scale());
|
||||
let shadow = self
|
||||
.shadow
|
||||
.get_tracking_invalidate(context)
|
||||
.into_px(context.gfx.scale());
|
||||
let shadow_spread = shadow.spread.into_unsigned();
|
||||
|
||||
let child_shadow_offset_amount = shadow.offset.abs().into_unsigned();
|
||||
|
|
@ -315,7 +316,8 @@ impl Widget for Container {
|
|||
.map(|padding| padding.get().into_px(context.kludgine.scale()))
|
||||
.unwrap_or_default();
|
||||
let shadow = self
|
||||
.effective_shadow(context)
|
||||
.shadow
|
||||
.get_tracking_invalidate(context)
|
||||
.into_px(context.kludgine.scale());
|
||||
|
||||
if shadow.offset.x >= 0 {
|
||||
|
|
|
|||
|
|
@ -149,16 +149,14 @@ impl Widget for Image {
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
self.scaling.invalidate_when_changed(context);
|
||||
|
||||
match self.scaling.get() {
|
||||
match self.scaling.get_tracking_invalidate(context) {
|
||||
ImageScaling::Aspect { .. } | ImageScaling::Stretch => {
|
||||
available_space.map(ConstraintLimit::min)
|
||||
}
|
||||
ImageScaling::Scale(factor) => {
|
||||
self.contents.invalidate_when_changed(context);
|
||||
self.contents.map(AnyTexture::size).map(|px| px * factor)
|
||||
}
|
||||
ImageScaling::Scale(factor) => self
|
||||
.contents
|
||||
.map_tracking_invalidate(context, AnyTexture::size)
|
||||
.map(|px| px * factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
235
src/widgets/wrap.rs
Normal file
235
src/widgets/wrap.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
//! A widget for laying out multiple widgets in a similar fashion as how words
|
||||
//! are wrapped in a paragraph.
|
||||
|
||||
use intentional::Cast;
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Zero};
|
||||
|
||||
use crate::context::{AsEventContext, GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::{IntrinsicPadding, LayoutOrder};
|
||||
use crate::styles::{FlexibleDimension, HorizontalOrder};
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widget::{Children, MountedChildren, Widget};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A widget that lays its children out horizontally, wrapping into multiple
|
||||
/// rows when the widgets can't fit.
|
||||
///
|
||||
/// This widget is designed to mimic how text layout occurs for words within a
|
||||
/// paragraph.
|
||||
#[derive(Debug)]
|
||||
pub struct Wrap {
|
||||
/// The children to wrap.
|
||||
pub children: Value<Children>,
|
||||
/// The horizontal alignment for widgets on the same row.
|
||||
pub align: Value<WrapAlign>,
|
||||
/// The vertical alignment for widgets on the same row.
|
||||
pub vertical_align: Value<VerticalAlign>,
|
||||
/// The spacing to place between widgets. When [`FlexibleDimension::Auto`]
|
||||
/// is set, [`IntrinsicPadding`] will be used.
|
||||
pub spacing: Value<Size<FlexibleDimension>>,
|
||||
mounted: MountedChildren,
|
||||
}
|
||||
|
||||
impl Wrap {
|
||||
/// Returns a new widget that wraps `children`.
|
||||
#[must_use]
|
||||
pub fn new(children: impl IntoValue<Children>) -> Self {
|
||||
Self {
|
||||
children: children.into_value(),
|
||||
align: Value::default(),
|
||||
vertical_align: Value::default(),
|
||||
spacing: Value::Constant(Size::squared(FlexibleDimension::Auto)),
|
||||
mounted: MountedChildren::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the spacing between widgets and returns self.
|
||||
#[must_use]
|
||||
pub fn spacing(mut self, spacing: impl IntoValue<Size<FlexibleDimension>>) -> Self {
|
||||
self.spacing = spacing.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the horizontal alignment and returns self.
|
||||
#[must_use]
|
||||
pub fn align(mut self, align: impl IntoValue<WrapAlign>) -> Self {
|
||||
self.align = align.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the vertical alignment and returns self.
|
||||
#[must_use]
|
||||
pub fn vertical_align(mut self, align: impl IntoValue<VerticalAlign>) -> Self {
|
||||
self.vertical_align = align.into_value();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Wrap {
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
for child in self.mounted.children() {
|
||||
context.for_other(child).redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
struct RowChild {
|
||||
index: usize,
|
||||
x: Px,
|
||||
size: Size<Px>,
|
||||
}
|
||||
|
||||
let order = context.get(&LayoutOrder).horizontal;
|
||||
|
||||
self.children.invalidate_when_changed(context);
|
||||
let align = self.align.get_tracking_invalidate(context);
|
||||
let vertical_align = self.vertical_align.get_tracking_invalidate(context);
|
||||
let spacing = self
|
||||
.spacing
|
||||
.get_tracking_invalidate(context)
|
||||
.map(|dimension| match dimension {
|
||||
FlexibleDimension::Auto => context.get(&IntrinsicPadding),
|
||||
FlexibleDimension::Dimension(dimension) => dimension,
|
||||
})
|
||||
.into_px(context.gfx.scale());
|
||||
self.mounted
|
||||
.synchronize_with(&self.children, &mut context.as_event_context());
|
||||
|
||||
let mut y = Px::ZERO;
|
||||
let mut row_children = Vec::new();
|
||||
let mut index = 0;
|
||||
let width = available_space.width.max().into_signed();
|
||||
let child_constraints =
|
||||
available_space.map(|limit| ConstraintLimit::SizeToFit(limit.max()));
|
||||
while index < self.mounted.children().len() {
|
||||
if y != Px::ZERO {
|
||||
y += spacing.height;
|
||||
}
|
||||
// Find all children that can fit on this next row.
|
||||
let mut x = Px::ZERO;
|
||||
let mut max_height = Px::ZERO;
|
||||
while let Some(child) = self.mounted.children().get(index) {
|
||||
let child_size = context
|
||||
.for_other(child)
|
||||
.layout(child_constraints)
|
||||
.into_signed();
|
||||
max_height = max_height.max(child_size.height);
|
||||
|
||||
let child_x = if x.is_zero() {
|
||||
x
|
||||
} else {
|
||||
x.saturating_add(spacing.width)
|
||||
};
|
||||
let after_child = child_x.saturating_add(child_size.width);
|
||||
|
||||
if x > 0 && after_child > width {
|
||||
break;
|
||||
}
|
||||
|
||||
row_children.push(RowChild {
|
||||
index,
|
||||
x: child_x,
|
||||
size: child_size,
|
||||
});
|
||||
|
||||
x = after_child;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
// Calculate the horizontal alignment.
|
||||
let remaining = (width - x).max(Px::ZERO);
|
||||
let (x, space_between) = if remaining > 0 {
|
||||
match (align, order) {
|
||||
(WrapAlign::Start, HorizontalOrder::LeftToRight)
|
||||
| (WrapAlign::End, HorizontalOrder::RightToLeft) => (Px::ZERO, Px::ZERO),
|
||||
(WrapAlign::End, HorizontalOrder::LeftToRight)
|
||||
| (WrapAlign::Start, HorizontalOrder::RightToLeft) => (remaining, Px::ZERO),
|
||||
(WrapAlign::Center, _) => (remaining / 2, Px::ZERO),
|
||||
(WrapAlign::SpaceBetween, _) => {
|
||||
if row_children.len() > 1 {
|
||||
(Px::ZERO, remaining / (row_children.len() - 1).cast::<i32>())
|
||||
} else {
|
||||
(Px::ZERO, Px::ZERO)
|
||||
}
|
||||
}
|
||||
(WrapAlign::SpaceEvenly, _) => {
|
||||
let spacing = remaining / row_children.len().cast::<i32>();
|
||||
(spacing / 2, spacing)
|
||||
}
|
||||
(WrapAlign::SpaceAround, _) => {
|
||||
let spacing = remaining / (row_children.len() + 1).cast::<i32>();
|
||||
(spacing, spacing)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(Px::ZERO, Px::ZERO)
|
||||
};
|
||||
|
||||
// Position the children
|
||||
let mut additional_x = x;
|
||||
for (child_index, child) in row_children.drain(..).enumerate() {
|
||||
if child_index > 0 {
|
||||
additional_x += space_between;
|
||||
}
|
||||
let child_x = additional_x + child.x;
|
||||
let child_y = y + match vertical_align {
|
||||
VerticalAlign::Top => Px::ZERO,
|
||||
VerticalAlign::Middle => (max_height - child.size.height) / 2,
|
||||
VerticalAlign::Bottom => max_height - child.size.height,
|
||||
};
|
||||
|
||||
context.set_child_layout(
|
||||
&self.mounted.children()[child.index],
|
||||
Rect::new(Point::new(child_x, child_y), child.size),
|
||||
);
|
||||
}
|
||||
|
||||
y += max_height;
|
||||
}
|
||||
|
||||
Size::new(width, y).into_unsigned()
|
||||
}
|
||||
}
|
||||
|
||||
/// The horizontal alignment to apply to widgets inside of a [`Wrap`].
|
||||
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum WrapAlign {
|
||||
/// Position the widgets at the start of the line, honoring [`LayoutOrder`].
|
||||
#[default]
|
||||
Start,
|
||||
/// Position the widgets at the end of the line, honoring [`LayoutOrder`].
|
||||
End,
|
||||
/// Position the widgets centered on the line.
|
||||
Center,
|
||||
/// Position the widgets evenly along the line with no space before the
|
||||
/// first widget or after the last widget.
|
||||
SpaceBetween,
|
||||
/// Position the widgets evenly along the line with half of the amount of
|
||||
/// spacing used between the widgets placed at the start and end of the
|
||||
/// line.
|
||||
SpaceEvenly,
|
||||
/// Position the widgets evenly along the line with an equal amount of
|
||||
/// spacing used between the widgets placed at the start and end of the
|
||||
/// line.
|
||||
SpaceAround,
|
||||
}
|
||||
|
||||
/// Alignment along the vertical axis.
|
||||
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum VerticalAlign {
|
||||
/// Align towards the top.
|
||||
Top,
|
||||
/// Align towards the middle/center.
|
||||
Middle,
|
||||
|
||||
/// Align towards the bottom.
|
||||
// TODO the default should be `Baseline`, but that requires a refactor for
|
||||
// layout() to return something other than a Size.
|
||||
#[default]
|
||||
Bottom,
|
||||
}
|
||||
|
|
@ -690,6 +690,12 @@ where
|
|||
),
|
||||
gfx: Exclusive::Owned(Graphics::new(graphics, &mut self.fonts)),
|
||||
};
|
||||
if self.initial_frame {
|
||||
self.root
|
||||
.lock()
|
||||
.as_widget()
|
||||
.mounted(&mut context.as_event_context());
|
||||
}
|
||||
self.theme_mode.redraw_when_changed(&context);
|
||||
let mut layout_context = LayoutContext::new(&mut context);
|
||||
let window_size = layout_context.gfx.size();
|
||||
|
|
|
|||
Loading…
Reference in a new issue