Resize now accepts ranges, Window honors Resize

Closes #62, Closes #63
This commit is contained in:
Jonathan Johnson 2023-11-09 14:54:41 -08:00
parent 1714948174
commit 724f6d7b18
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
12 changed files with 319 additions and 58 deletions

10
Cargo.lock generated
View file

@ -855,7 +855,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kludgine"
version = "0.1.0"
source = "git+https://github.com/khonsulabs/kludgine#fd2078efb7212db0f07120b80440699b00ec2a2b"
source = "git+https://github.com/khonsulabs/kludgine#5d728e775b9bf64ac30e1e673c9971fc2184cb97"
dependencies = [
"ahash",
"alot",
@ -923,9 +923,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
[[package]]
name = "lock_api"
@ -1572,9 +1572,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.11.1"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
name = "smithay-client-toolkit"

View file

@ -21,7 +21,7 @@ tracing-subscriber = { version = "0.3", optional = true }
# [patch."https://github.com/khonsulabs/kludgine"]
# kludgine = { path = "../kludgine2" }
# kludgine = { path = "../kludgine" }
# [patch."https://github.com/khonsulabs/appit"]
# appit = { path = "../appit" }
# [patch."https://github.com/khonsulabs/figures"]

View file

@ -43,8 +43,7 @@ fn main() -> gooey::Result {
);
Resize::width(
// TODO We need a min/max range for the Resize widget
Lp::points(400),
Lp::points(300)..Lp::points(600),
Stack::rows(username_row.and(password_row).and(buttons)),
)
.centered()

View file

@ -1,5 +1,5 @@
use gooey::styles::components::TextColor;
use gooey::widget::{MakeWidget, Widget};
use gooey::widget::MakeWidget;
use gooey::widgets::stack::Stack;
use gooey::widgets::{Button, Style};
use gooey::Run;
@ -12,6 +12,6 @@ fn main() -> gooey::Result {
}
/// Creating reusable style helpers that work with any Widget is straightfoward
fn red_text(w: impl Widget) -> Style {
fn red_text(w: impl MakeWidget) -> Style {
w.with(&TextColor, Color::RED)
}

View file

@ -2,7 +2,9 @@
use std::borrow::Cow;
use std::collections::{hash_map, HashMap};
use std::ops::Add;
use std::ops::{
Add, Bound, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
};
use std::sync::Arc;
use crate::animation::{EasingFunction, ZeroToOne};
@ -172,8 +174,8 @@ use std::any::Any;
use std::fmt::Debug;
use std::panic::{RefUnwindSafe, UnwindSafe};
use kludgine::figures::units::{Lp, Px};
use kludgine::figures::{ScreenScale, Size};
use kludgine::figures::units::{Lp, Px, UPx};
use kludgine::figures::{Fraction, IntoUnsigned, ScreenScale, Size};
use kludgine::Color;
/// A value of a style component.
@ -183,6 +185,8 @@ pub enum Component {
Color(Color),
/// A single-dimension measurement.
Dimension(Dimension),
/// A single-dimension measurement.
DimensionRange(DimensionRange),
/// A percentage between 0.0 and 1.0.
Percent(ZeroToOne),
/// A custom component type.
@ -302,7 +306,7 @@ impl From<Lp> for FlexibleDimension {
}
/// A 1-dimensional measurement.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Dimension {
/// Physical Pixels
Px(Px),
@ -360,6 +364,158 @@ impl ScreenScale for Dimension {
}
}
/// A range of [`Dimension`]s.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct DimensionRange {
/// The start bound of the range.
pub start: Bound<Dimension>,
/// The end bound of the range.
pub end: Bound<Dimension>,
}
impl DimensionRange {
/// Returns this range's dimension if the range represents a single
/// dimension.
#[must_use]
pub fn exact_dimension(&self) -> Option<Dimension> {
match (self.start, self.end) {
(Bound::Excluded(start), Bound::Included(end)) if start == end => Some(start),
_ => None,
}
}
/// Clamps `size` to the dimensions of this range, converting to unsigned
/// pixels in the process.
#[must_use]
pub fn clamp(&self, mut size: UPx, scale: Fraction) -> UPx {
if let Some(min) = self.minimum() {
size = size.max(min.into_px(scale).into_unsigned());
}
if let Some(max) = self.maximum() {
size = size.min(max.into_px(scale).into_unsigned());
}
size
}
/// Returns the minimum measurement, if the start is bounded.
#[must_use]
pub fn minimum(&self) -> Option<Dimension> {
match self.start {
Bound::Unbounded => None,
Bound::Excluded(Dimension::Lp(lp)) => Some(Dimension::Lp(lp + 1)),
Bound::Excluded(Dimension::Px(px)) => Some(Dimension::Px(px + 1)),
Bound::Included(value) => Some(value),
}
}
/// Returns the maximum measurement, if the end is bounded.
#[must_use]
pub fn maximum(&self) -> Option<Dimension> {
match self.end {
Bound::Unbounded => None,
Bound::Excluded(Dimension::Lp(lp)) => Some(Dimension::Lp(lp - 1)),
Bound::Excluded(Dimension::Px(px)) => Some(Dimension::Px(px - 1)),
Bound::Included(value) => Some(value),
}
}
}
impl<T> From<T> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: T) -> Self {
let dimension = value.into();
Self::from(dimension..=dimension)
}
}
impl<T> From<Range<T>> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: Range<T>) -> Self {
Self {
start: Bound::Included(value.start.into()),
end: Bound::Excluded(value.end.into()),
}
}
}
impl From<RangeFull> for DimensionRange {
fn from(_: RangeFull) -> Self {
Self {
start: Bound::Unbounded,
end: Bound::Unbounded,
}
}
}
impl<T> From<RangeInclusive<T>> for DimensionRange
where
T: Into<Dimension> + Clone,
{
fn from(value: RangeInclusive<T>) -> Self {
Self {
start: Bound::Included(value.start().clone().into()),
end: Bound::Excluded(value.end().clone().into()),
}
}
}
impl<T> From<RangeFrom<T>> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: RangeFrom<T>) -> Self {
Self {
start: Bound::Included(value.start.into()),
end: Bound::Unbounded,
}
}
}
impl<T> From<RangeTo<T>> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: RangeTo<T>) -> Self {
Self {
start: Bound::Unbounded,
end: Bound::Excluded(value.end.into()),
}
}
}
impl<T> From<RangeToInclusive<T>> for DimensionRange
where
T: Into<Dimension>,
{
fn from(value: RangeToInclusive<T>) -> Self {
Self {
start: Bound::Unbounded,
end: Bound::Included(value.end.into()),
}
}
}
impl From<DimensionRange> for Component {
fn from(value: DimensionRange) -> Self {
Component::DimensionRange(value)
}
}
impl TryFrom<Component> for DimensionRange {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::DimensionRange(value) => Ok(value),
other => Err(other),
}
}
}
/// A custom component value.
#[derive(Debug, Clone)]
pub struct CustomComponent(Arc<dyn AnyComponent>);

View file

@ -156,6 +156,13 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static {
) -> EventHandling {
IGNORED
}
/// Returns a reference to a single child widget if this widget is a widget
/// that primarily wraps a single other widget to customize its behavior.
#[must_use]
fn wraps(&mut self) -> Option<&WidgetInstance> {
None
}
}
impl<T> Run for T
@ -170,7 +177,7 @@ where
/// A [`Widget`] that contains a single child.
pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
/// Returns the child widget.
fn child(&mut self) -> &mut WidgetRef;
fn child_mut(&mut self) -> &mut WidgetRef;
/// Returns the rectangle that the child widget should occupy given
/// `available_space`.
@ -180,7 +187,7 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<Px> {
let child = self.child().mounted(&mut context.as_event_context());
let child = self.child_mut().mounted(&mut context.as_event_context());
context
.for_other(&child)
@ -312,8 +319,12 @@ impl<T> Widget for T
where
T: WrapperWidget,
{
fn wraps(&mut self) -> Option<&WidgetInstance> {
Some(self.child_mut().widget())
}
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
let child = self.child().mounted(&mut context.as_event_context());
let child = self.child_mut().mounted(&mut context.as_event_context());
context.for_other(&child).redraw();
}
@ -322,7 +333,7 @@ where
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<UPx> {
let child = self.child().mounted(&mut context.as_event_context());
let child = self.child_mut().mounted(&mut context.as_event_context());
let layout = self.layout_child(available_space, context);
context.set_child_layout(&child, layout);
@ -1121,6 +1132,15 @@ impl WidgetRef {
};
widget.clone()
}
/// Returns the a reference to the underlying widget instance.
#[must_use]
pub fn widget(&self) -> &WidgetInstance {
match self {
WidgetRef::Unmounted(widget) => widget,
WidgetRef::Mounted(managed) => &managed.widget,
}
}
}
impl AsRef<WidgetId> for WidgetRef {

View file

@ -178,7 +178,7 @@ impl FrameInfo {
}
impl WrapperWidget for Align {
fn child(&mut self) -> &mut WidgetRef {
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child
}

View file

@ -63,7 +63,7 @@ impl Expand {
}
impl WrapperWidget for Expand {
fn child(&mut self) -> &mut WidgetRef {
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child
}

View file

@ -1,17 +1,17 @@
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Rect, ScreenScale, Size};
use crate::context::{AsEventContext, LayoutContext};
use crate::styles::Dimension;
use crate::styles::DimensionRange;
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
use crate::ConstraintLimit;
/// A widget that resizes its contained widget to an explicit size.
#[derive(Debug)]
pub struct Resize {
/// If present, the width to apply to the child widget.
pub width: Option<Dimension>,
/// If present, the height to apply to the child widget.
pub height: Option<Dimension>,
/// The range of allowed width for the child widget.
pub width: DimensionRange,
/// The range of allowed height for the child widget.
pub height: DimensionRange,
child: WidgetRef,
}
@ -26,38 +26,38 @@ impl Resize {
#[must_use]
pub fn to<T>(size: Size<T>, child: impl MakeWidget) -> Self
where
T: Into<Dimension>,
T: Into<DimensionRange>,
{
Self {
child: WidgetRef::new(child),
width: Some(size.width.into()),
height: Some(size.height.into()),
width: size.width.into(),
height: size.height.into(),
}
}
/// Resizes `child`'s width to `width`.
#[must_use]
pub fn width(width: impl Into<Dimension>, child: impl MakeWidget) -> Self {
pub fn width(width: impl Into<DimensionRange>, child: impl MakeWidget) -> Self {
Self {
child: WidgetRef::new(child),
width: Some(width.into()),
height: None,
width: width.into(),
height: DimensionRange::from(..),
}
}
/// Resizes `child`'s height to `height`.
#[must_use]
pub fn height(height: impl Into<Dimension>, child: impl MakeWidget) -> Self {
pub fn height(height: impl Into<DimensionRange>, child: impl MakeWidget) -> Self {
Self {
child: WidgetRef::new(child),
width: None,
height: Some(height.into()),
width: DimensionRange::from(..),
height: height.into(),
}
}
}
impl WrapperWidget for Resize {
fn child(&mut self) -> &mut WidgetRef {
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child
}
@ -67,7 +67,9 @@ impl WrapperWidget for Resize {
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<kludgine::figures::units::Px> {
let child = self.child.mounted(&mut context.as_event_context());
let size = if let (Some(width), Some(height)) = (self.width, self.height) {
let size = if let (Some(width), Some(height)) =
(self.width.exact_dimension(), self.height.exact_dimension())
{
Size::new(
width.into_px(context.gfx.scale()).into_unsigned(),
height.into_px(context.gfx.scale()).into_unsigned(),
@ -85,12 +87,13 @@ impl WrapperWidget for Resize {
fn override_constraint(
constraint: ConstraintLimit,
explicit: Option<Dimension>,
range: DimensionRange,
scale: Fraction,
) -> ConstraintLimit {
if let Some(size) = explicit {
ConstraintLimit::Known(size.into_px(scale).into_unsigned())
} else {
constraint
match constraint {
ConstraintLimit::Known(size) => ConstraintLimit::Known(range.clamp(size, scale)),
ConstraintLimit::ClippedAfter(clipped_after) => {
ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale))
}
}
}

View file

@ -1,7 +1,7 @@
//! A widget that combines a collection of [`Children`] widgets into one.
// TODO on scale change, all `Lp` children need to resize
use std::ops::Deref;
use std::ops::{Bound, Deref};
use alot::{LotId, OrderedLots};
use kludgine::figures::units::{Lp, UPx};
@ -95,14 +95,22 @@ impl Stack {
)
} else if let Some((child, size)) =
guard.downcast_ref::<Resize>().and_then(|r| {
match self.layout.orientation.orientation {
let range = match self.layout.orientation.orientation {
StackOrientation::Row => r.height,
StackOrientation::Column => r.width,
}
.map(|size| (r.child().clone(), size))
};
range.minimum().map(|min| {
(
r.child().clone(),
StackDimension::Measured {
min,
_max: range.end,
},
)
})
})
{
(child, StackDimension::Exact(size))
(child, size)
} else {
(
WidgetRef::Unmounted(widget.clone()),
@ -260,7 +268,7 @@ pub enum StackOrientation {
/// The strategy to use when laying a widget out inside of an [`Stack`].
#[derive(Debug, Clone, Copy)]
pub enum StackDimension {
enum StackDimension {
/// Attempt to lay out the widget based on its contents.
FitContent,
/// Use a fractional amount of the available space.
@ -269,8 +277,13 @@ pub enum StackDimension {
/// fractionally.
weight: u8,
},
/// Use an exact measurement for this widget's size.
Exact(Dimension),
/// Use a range for this widget's size.
Measured {
/// The minimum size for the widget.
min: Dimension,
/// The optional maximum size for the widget.
_max: Bound<Dimension>,
},
}
#[derive(Debug)]
@ -322,7 +335,7 @@ impl Layout {
self.fractional.retain(|(measured, _)| *measured != id);
self.total_weights -= u32::from(weight);
}
StackDimension::Exact(size) => match size {
StackDimension::Measured { min, .. } => match min {
Dimension::Px(pixels) => {
self.allocated_space.0 -= pixels.into_unsigned();
}
@ -357,12 +370,12 @@ impl Layout {
self.fractional.push((id, weight));
UPx(0)
}
StackDimension::Exact(size) => {
match size {
StackDimension::Measured { min, .. } => {
match min {
Dimension::Px(size) => self.allocated_space.0 += size.into_unsigned(),
Dimension::Lp(size) => self.allocated_space.1 += size,
}
size.into_px(scale).into_unsigned()
min.into_px(scale).into_unsigned()
}
};
self.layouts.insert(
@ -464,6 +477,7 @@ impl Deref for Layout {
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use std::ops::Bound;
use kludgine::figures::units::UPx;
use kludgine::figures::{Fraction, IntoSigned, Size};
@ -490,7 +504,10 @@ mod tests {
}
pub fn fixed_size(mut self, size: UPx) -> Self {
self.dimension = StackDimension::Exact(Dimension::Px(size.into_signed()));
self.dimension = StackDimension::Measured {
min: Dimension::Px(size.into_signed()),
_max: Bound::Unbounded,
};
self
}

View file

@ -21,7 +21,7 @@ impl Style {
}
impl WrapperWidget for Style {
fn child(&mut self) -> &mut WidgetRef {
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child
}

View file

@ -10,14 +10,14 @@ use std::path::Path;
use std::string::ToString;
use std::sync::OnceLock;
use kludgine::app::winit::dpi::PhysicalPosition;
use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize};
use kludgine::app::winit::event::{
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
};
use kludgine::app::winit::keyboard::Key;
use kludgine::app::WindowBehavior as _;
use kludgine::figures::units::Px;
use kludgine::figures::{IntoSigned, Point, Rect, Size};
use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
use kludgine::render::Drawing;
use kludgine::Kludgine;
use tracing::Level;
@ -34,6 +34,7 @@ use crate::value::{Dynamic, IntoDynamic};
use crate::widget::{
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
};
use crate::widgets::Resize;
use crate::window::sealed::WindowCommand;
use crate::{initialize_tracing, ConstraintLimit, Run};
@ -248,6 +249,8 @@ struct GooeyWindow<T> {
occluded: Dynamic<bool>,
focused: Dynamic<bool>,
keyboard_activated: Option<ManagedWidget>,
min_inner_size: Option<Size<UPx>>,
max_inner_size: Option<Size<UPx>>,
}
impl<T> GooeyWindow<T>
@ -337,6 +340,8 @@ where
occluded,
focused,
keyboard_activated: None,
min_inner_size: None,
max_inner_size: None,
}
}
@ -348,6 +353,55 @@ where
self.redraw_status.refresh_received();
graphics.reset_text_attributes();
self.root.tree.reset_render_order();
let resizable = window.winit().is_resizable();
{
let mut root_or_child = self.root.widget.clone();
loop {
let mut widget = root_or_child.lock();
if let Some(resize) = widget.downcast_ref::<Resize>() {
let min_width = resize
.width
.minimum()
.map_or(Px(0), |width| width.into_px(graphics.scale()));
let max_width = resize
.width
.maximum()
.map_or(Px::MAX, |width| width.into_px(graphics.scale()));
let min_height = resize
.height
.minimum()
.map_or(Px(0), |height| height.into_px(graphics.scale()));
let max_height = resize
.height
.maximum()
.map_or(Px::MAX, |height| height.into_px(graphics.scale()));
let new_min_size = (min_width > 0 || min_height > 0)
.then_some(Size::<Px>::new(min_width, min_height).into_unsigned());
if new_min_size != self.min_inner_size {
window.set_min_inner_size(new_min_size);
self.min_inner_size = new_min_size;
}
let new_max_size = (max_width > 0 || max_height > 0)
.then_some(Size::<Px>::new(max_width, max_height).into_unsigned());
if new_max_size != self.max_inner_size && resizable {
window.set_max_inner_size(new_max_size);
}
self.max_inner_size = new_max_size;
break;
} else if let Some(wraps) = widget.as_widget().wraps().cloned() {
drop(widget);
root_or_child = wraps;
} else {
break;
}
}
}
let graphics = self.contents.new_frame(graphics);
let mut window = RunningWindow::new(window, &self.focused, &self.occluded);
let mut context = GraphicsContext {
@ -361,6 +415,18 @@ where
ConstraintLimit::ClippedAfter(window_size.height),
));
let render_size = actual_size.min(window_size);
if render_size != window_size && !resizable {
let mut new_size = actual_size;
if let Some(min_size) = self.min_inner_size {
new_size = new_size.max(min_size);
}
if let Some(max_size) = self.max_inner_size {
new_size = new_size.min(max_size);
}
let _ = layout_context
.winit()
.request_inner_size(PhysicalSize::from(new_size));
}
self.root.set_layout(Rect::from(render_size.into_signed()));
if self.initial_frame {