mirror of
https://github.com/danbulant/cushy
synced 2026-06-08 00:50:34 +00:00
Spacing widget
This commit is contained in:
parent
ed31805693
commit
a95a7dc850
6 changed files with 364 additions and 4 deletions
|
|
@ -1,13 +1,13 @@
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
use gooey::value::Dynamic;
|
use gooey::value::Dynamic;
|
||||||
use gooey::widgets::{Button, Label, Scroll, Stack};
|
use gooey::widgets::{Button, Label, Spacing, Stack};
|
||||||
use gooey::{widgets, Run};
|
use gooey::{widgets, Run};
|
||||||
|
|
||||||
fn main() -> gooey::Result {
|
fn main() -> gooey::Result {
|
||||||
let counter = Dynamic::new(0i32);
|
let counter = Dynamic::new(0i32);
|
||||||
let label = counter.map_each(ToString::to_string);
|
let label = counter.map_each(ToString::to_string);
|
||||||
Scroll::new(Stack::columns(widgets![
|
Spacing::auto(Stack::columns(widgets![
|
||||||
Label::new(label),
|
Label::new(label),
|
||||||
Button::new("+").on_click(counter.with_clone(|counter| {
|
Button::new("+").on_click(counter.with_clone(|counter| {
|
||||||
move |_| {
|
move |_| {
|
||||||
|
|
|
||||||
15
src/lib.rs
15
src/lib.rs
|
|
@ -14,6 +14,8 @@ pub mod value;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
use std::ops::Sub;
|
||||||
|
|
||||||
pub use with_clone::WithClone;
|
pub use with_clone::WithClone;
|
||||||
mod with_clone;
|
mod with_clone;
|
||||||
|
|
||||||
|
|
@ -44,6 +46,19 @@ impl ConstraintLimit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Sub<UPx> for ConstraintLimit {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: UPx) -> Self::Output {
|
||||||
|
match self {
|
||||||
|
ConstraintLimit::Known(px) => ConstraintLimit::Known(px.saturating_sub(rhs)),
|
||||||
|
ConstraintLimit::ClippedAfter(px) => {
|
||||||
|
ConstraintLimit::ClippedAfter(px.saturating_sub(rhs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A result alias that defaults to the result type commonly used throughout
|
/// A result alias that defaults to the result type commonly used throughout
|
||||||
/// this crate.
|
/// this crate.
|
||||||
pub type Result<T = (), E = EventLoopError> = std::result::Result<T, E>;
|
pub type Result<T = (), E = EventLoopError> = std::result::Result<T, E>;
|
||||||
|
|
|
||||||
164
src/styles.rs
164
src/styles.rs
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::{hash_map, HashMap};
|
use std::collections::{hash_map, HashMap};
|
||||||
|
use std::ops::Add;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::names::Name;
|
use crate::names::Name;
|
||||||
use crate::utils::Lazy;
|
use crate::utils::Lazy;
|
||||||
|
use crate::value::{IntoValue, Value};
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
|
|
||||||
|
|
@ -132,7 +134,7 @@ use std::fmt::Debug;
|
||||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
|
||||||
use kludgine::figures::units::{Lp, Px};
|
use kludgine::figures::units::{Lp, Px};
|
||||||
use kludgine::figures::ScreenScale;
|
use kludgine::figures::{ScreenScale, Size};
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
|
|
||||||
/// A value of a style component.
|
/// A value of a style component.
|
||||||
|
|
@ -216,6 +218,39 @@ impl TryFrom<Component> for Lp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A 1-dimensional measurement that may be automatically calculated.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum FlexibleDimension {
|
||||||
|
/// Automatically calculate this dimension.
|
||||||
|
Auto,
|
||||||
|
/// Use this dimension.
|
||||||
|
Dimension(Dimension),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FlexibleDimension {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Dimension(Dimension::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Dimension> for FlexibleDimension {
|
||||||
|
fn from(dimension: Dimension) -> Self {
|
||||||
|
Self::Dimension(dimension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Px> for FlexibleDimension {
|
||||||
|
fn from(value: Px) -> Self {
|
||||||
|
Self::from(Dimension::from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Lp> for FlexibleDimension {
|
||||||
|
fn from(value: Lp) -> Self {
|
||||||
|
Self::from(Dimension::from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A 1-dimensional measurement.
|
/// A 1-dimensional measurement.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Dimension {
|
pub enum Dimension {
|
||||||
|
|
@ -453,3 +488,130 @@ impl NamedComponent for Cow<'_, ComponentName> {
|
||||||
Cow::Borrowed(self)
|
Cow::Borrowed(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A type describing characteristics about the edges of a rectangle.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Edges<T = FlexibleDimension> {
|
||||||
|
/// The left edge
|
||||||
|
pub left: T,
|
||||||
|
/// The right edge
|
||||||
|
pub right: T,
|
||||||
|
/// The top edge
|
||||||
|
pub top: T,
|
||||||
|
/// The bottom edge
|
||||||
|
pub bottom: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Edges<T> {
|
||||||
|
/// Returns the sum of the parts as a [`Size`].
|
||||||
|
pub fn size(&self) -> Size<T>
|
||||||
|
where
|
||||||
|
T: Add<Output = T> + Copy,
|
||||||
|
{
|
||||||
|
Size::new(self.left + self.right, self.top + self.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for Edges<T>
|
||||||
|
where
|
||||||
|
T: Default,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
left: T::default(),
|
||||||
|
right: T::default(),
|
||||||
|
top: T::default(),
|
||||||
|
bottom: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Edges<T> {
|
||||||
|
/// Updates `top` and returns self.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_top(mut self, top: impl Into<T>) -> Self {
|
||||||
|
self.top = top.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates `bottom` and returns self.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_bottom(mut self, bottom: impl Into<T>) -> Self {
|
||||||
|
self.bottom = bottom.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates `right` and returns self.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_right(mut self, right: impl Into<T>) -> Self {
|
||||||
|
self.right = right.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates `left` and returns self.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_left(mut self, left: impl Into<T>) -> Self {
|
||||||
|
self.left = left.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates left and right to be `horizontal` and returns self.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_horizontal(mut self, horizontal: impl Into<T>) -> Self
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
{
|
||||||
|
self.left = horizontal.into();
|
||||||
|
self.right = self.left.clone();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates top and bottom to be `vertical` and returns self.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_vertical(mut self, vertical: impl Into<T>) -> Self
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
{
|
||||||
|
self.top = vertical.into();
|
||||||
|
self.bottom = self.top.clone();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Edges<Dimension> {
|
||||||
|
/// Returns a new instance with `dimension` for every edge.
|
||||||
|
#[must_use]
|
||||||
|
pub fn uniform<D>(dimension: D) -> Self
|
||||||
|
where
|
||||||
|
D: Into<Dimension>,
|
||||||
|
{
|
||||||
|
let dimension = dimension.into();
|
||||||
|
Self::from(dimension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Edges<T>
|
||||||
|
where
|
||||||
|
T: Clone,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
left: value.clone(),
|
||||||
|
right: value.clone(),
|
||||||
|
top: value.clone(),
|
||||||
|
bottom: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoValue<Edges<FlexibleDimension>> for FlexibleDimension {
|
||||||
|
fn into_value(self) -> Value<Edges<FlexibleDimension>> {
|
||||||
|
Value::Constant(Edges::from(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoValue<Edges<Dimension>> for Dimension {
|
||||||
|
fn into_value(self) -> Value<Edges<Dimension>> {
|
||||||
|
Value::Constant(Edges::from(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ impl ComponentDefinition for HighlightColor {
|
||||||
///
|
///
|
||||||
/// This component is opt-in and does not automatically work for all widgets. To
|
/// This component is opt-in and does not automatically work for all widgets. To
|
||||||
/// apply arbitrary, non-uniform padding around another widget, use a
|
/// apply arbitrary, non-uniform padding around another widget, use a
|
||||||
/// [`Cell`](crate::widgets::Cell).
|
/// [`Spacing`](crate::widgets::Spacing).
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
pub struct IntrinsicPadding;
|
pub struct IntrinsicPadding;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ mod canvas;
|
||||||
mod input;
|
mod input;
|
||||||
mod label;
|
mod label;
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
|
mod spacing;
|
||||||
pub mod stack;
|
pub mod stack;
|
||||||
mod style;
|
mod style;
|
||||||
mod tilemap;
|
mod tilemap;
|
||||||
|
|
@ -14,6 +15,7 @@ pub use canvas::Canvas;
|
||||||
pub use input::Input;
|
pub use input::Input;
|
||||||
pub use label::Label;
|
pub use label::Label;
|
||||||
pub use scroll::Scroll;
|
pub use scroll::Scroll;
|
||||||
|
pub use spacing::Spacing;
|
||||||
pub use stack::Stack;
|
pub use stack::Stack;
|
||||||
pub use style::Style;
|
pub use style::Style;
|
||||||
pub use tilemap::TileMap;
|
pub use tilemap::TileMap;
|
||||||
|
|
|
||||||
181
src/widgets/spacing.rs
Normal file
181
src/widgets/spacing.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use kludgine::figures::units::UPx;
|
||||||
|
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||||
|
|
||||||
|
use crate::context::{AsEventContext, EventContext, GraphicsContext};
|
||||||
|
use crate::styles::{Edges, FlexibleDimension};
|
||||||
|
use crate::value::{IntoValue, Value};
|
||||||
|
use crate::widget::{MakeWidget, ManagedWidget, Widget, WidgetInstance};
|
||||||
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
|
/// A widget that provides spacing (padding) around its contents.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Spacing {
|
||||||
|
child: WidgetInstance,
|
||||||
|
mounted: Option<ManagedWidget>,
|
||||||
|
edges: Value<Edges<FlexibleDimension>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Spacing {
|
||||||
|
/// Returns a new spacing widget containing `widget`, surrounding it with
|
||||||
|
/// `margin`.
|
||||||
|
pub fn new(margin: impl IntoValue<Edges<FlexibleDimension>>, widget: impl MakeWidget) -> Self {
|
||||||
|
Self {
|
||||||
|
child: widget.make_widget(),
|
||||||
|
mounted: None,
|
||||||
|
edges: margin.into_value(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new spacing widget that centers `widget` vertically and
|
||||||
|
/// horizontally.
|
||||||
|
pub fn auto(widget: impl MakeWidget) -> Self {
|
||||||
|
Self::new(FlexibleDimension::Auto, widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget {
|
||||||
|
if self.mounted.is_none() {
|
||||||
|
self.mounted = Some(context.push_child(self.child.clone()));
|
||||||
|
}
|
||||||
|
self.mounted.as_ref().expect("always initialized").clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(
|
||||||
|
&mut self,
|
||||||
|
available_space: Size<ConstraintLimit>,
|
||||||
|
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
|
||||||
|
) -> Layout {
|
||||||
|
let margin = self.edges.get();
|
||||||
|
let vertical = FrameInfo::new(context.graphics.scale(), margin.top, margin.bottom);
|
||||||
|
let horizontal = FrameInfo::new(context.graphics.scale(), margin.left, margin.right);
|
||||||
|
|
||||||
|
let content_available = Size::new(
|
||||||
|
horizontal.child_constraint(available_space.width),
|
||||||
|
vertical.child_constraint(available_space.height),
|
||||||
|
);
|
||||||
|
|
||||||
|
let child = self.child(&mut context.as_event_context());
|
||||||
|
let content_size = context.for_other(&child).measure(content_available);
|
||||||
|
|
||||||
|
let (left, right, width) = horizontal.measure(available_space.width, content_size.width);
|
||||||
|
let (top, bottom, height) = vertical.measure(available_space.height, content_size.height);
|
||||||
|
|
||||||
|
Layout {
|
||||||
|
margin: Edges {
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
},
|
||||||
|
content: Size::new(width, height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FrameInfo {
|
||||||
|
a: Option<UPx>,
|
||||||
|
b: Option<UPx>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameInfo {
|
||||||
|
fn new(scale: Fraction, a: FlexibleDimension, b: FlexibleDimension) -> Self {
|
||||||
|
let a = match a {
|
||||||
|
FlexibleDimension::Auto => None,
|
||||||
|
FlexibleDimension::Dimension(dimension) => {
|
||||||
|
Some(dimension.into_px(scale).into_unsigned())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let b = match b {
|
||||||
|
FlexibleDimension::Auto => None,
|
||||||
|
FlexibleDimension::Dimension(dimension) => {
|
||||||
|
Some(dimension.into_px(scale).into_unsigned())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Self { a, b }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child_constraint(&self, available: ConstraintLimit) -> ConstraintLimit {
|
||||||
|
match (self.a, self.b) {
|
||||||
|
(Some(a), Some(b)) => available - (a + b),
|
||||||
|
// If we have at least one auto-measurement, force the constraint
|
||||||
|
// into ClippedAfter mode to make the widget attempt to size the
|
||||||
|
// content to fit.
|
||||||
|
(Some(one), None) | (None, Some(one)) => {
|
||||||
|
ConstraintLimit::ClippedAfter(available.max() - one)
|
||||||
|
}
|
||||||
|
(None, None) => ConstraintLimit::ClippedAfter(available.max()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&self, available: ConstraintLimit, content: UPx) -> (UPx, UPx, UPx) {
|
||||||
|
match available {
|
||||||
|
ConstraintLimit::Known(size) => {
|
||||||
|
let remaining = size - content;
|
||||||
|
let (a, b) = match (self.a, self.b) {
|
||||||
|
(Some(a), Some(b)) => (a, b),
|
||||||
|
(Some(a), None) => (a, remaining - a),
|
||||||
|
(None, Some(b)) => (remaining - b, b),
|
||||||
|
(None, None) => {
|
||||||
|
let a = remaining / 2;
|
||||||
|
let b = remaining - a;
|
||||||
|
(a, b)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(a, b, size - a - b)
|
||||||
|
}
|
||||||
|
ConstraintLimit::ClippedAfter(_) => (
|
||||||
|
self.a.unwrap_or_default(),
|
||||||
|
self.b.unwrap_or_default(),
|
||||||
|
content,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Spacing {
|
||||||
|
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||||
|
let layout = self.measure(
|
||||||
|
Size::new(
|
||||||
|
ConstraintLimit::Known(context.graphics.size().width),
|
||||||
|
ConstraintLimit::Known(context.graphics.size().height),
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
let child = self.child(&mut context.as_event_context());
|
||||||
|
context
|
||||||
|
.for_child(
|
||||||
|
&child,
|
||||||
|
Rect::new(
|
||||||
|
Point::new(
|
||||||
|
layout.margin.left.into_signed(),
|
||||||
|
layout.margin.top.into_signed(),
|
||||||
|
),
|
||||||
|
layout.content.into_signed(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(
|
||||||
|
&mut self,
|
||||||
|
available_space: Size<ConstraintLimit>,
|
||||||
|
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
|
||||||
|
) -> Size<UPx> {
|
||||||
|
self.measure(available_space, context)
|
||||||
|
.size()
|
||||||
|
.into_unsigned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Layout {
|
||||||
|
margin: Edges<UPx>,
|
||||||
|
content: Size<UPx>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
pub fn size(&self) -> Size<UPx> {
|
||||||
|
self.margin.size()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue