mirror of
https://github.com/danbulant/cushy
synced 2026-06-20 06:51:07 +00:00
Checkbox, ButtonKind, linked/linked_string + more
This commit is contained in:
parent
eb063c82f0
commit
54e01f1911
23 changed files with 801 additions and 176 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
|
@ -309,10 +309,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.84"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -564,7 +565,7 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
|||
[[package]]
|
||||
name = "figures"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/figures#7b41393c44d4def606790e340c98450b603010b4"
|
||||
source = "git+https://github.com/khonsulabs/figures#52d06f3623cdb47128f1537fdadfe190f7afa88e"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"euclid",
|
||||
|
|
@ -955,6 +956,15 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.65"
|
||||
|
|
@ -990,7 +1000,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
|||
[[package]]
|
||||
name = "kludgine"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#09790aafb5a9c3b0da034387adead9960eb06bc7"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#c0135da62309896fab58a2f5b517c32cde151fb3"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"alot",
|
||||
|
|
@ -1828,9 +1838,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.23"
|
||||
version = "0.38.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffb93593068e9babdad10e4fce47dc9b3ac25315a72a59766ffd9e9a71996a04"
|
||||
checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
|
|
@ -2941,18 +2951,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
|
|||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.25"
|
||||
version = "0.7.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
|
||||
checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.25"
|
||||
version = "0.7.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
|
||||
checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -1,29 +1,49 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::button::ButtonOutline;
|
||||
use gooey::widgets::Button;
|
||||
use gooey::widgets::button::ButtonKind;
|
||||
use gooey::widgets::{Button, Checkbox};
|
||||
use gooey::Run;
|
||||
use kludgine::Color;
|
||||
|
||||
// begin rustme snippet: readme
|
||||
fn main() -> gooey::Result {
|
||||
// Create a dynamic usize.
|
||||
let count = Dynamic::new(0_isize);
|
||||
let clicked_label = Dynamic::new(String::from("Click a Button"));
|
||||
let default_is_outline = Dynamic::new(false);
|
||||
let default_button_style = default_is_outline.map_each(|is_outline| {
|
||||
if *is_outline {
|
||||
ButtonKind::Outline
|
||||
} else {
|
||||
ButtonKind::Solid
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new button with a label that is produced by mapping the contents
|
||||
// of `count`.
|
||||
Button::new(count.map_each(ToString::to_string))
|
||||
// Set the `on_click` callback to a closure that increments the counter.
|
||||
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))
|
||||
clicked_label
|
||||
.clone()
|
||||
.and(
|
||||
// Creates a second, outlined button
|
||||
Button::new(count.map_each(ToString::to_string))
|
||||
// Set the `on_click` callback to a closure that decrements the counter.
|
||||
.on_click(count.with_clone(|count| move |_| count.set(count.get() - 1)))
|
||||
.with(&ButtonOutline, Color::DARKRED),
|
||||
Button::new("Normal Button")
|
||||
.on_click(
|
||||
clicked_label.with_clone(|label| {
|
||||
move |_| label.set(String::from("Clicked Normal Button"))
|
||||
}),
|
||||
)
|
||||
.and(
|
||||
Button::new("Outline Button")
|
||||
.on_click(clicked_label.with_clone(|label| {
|
||||
move |_| label.set(String::from("Clicked Outline Button"))
|
||||
}))
|
||||
.kind(ButtonKind::Outline),
|
||||
)
|
||||
.and(
|
||||
Button::new("Default Button")
|
||||
.on_click(clicked_label.with_clone(|label| {
|
||||
move |_| label.set(String::from("Clicked Default Button"))
|
||||
}))
|
||||
.kind(default_button_style)
|
||||
.into_default(),
|
||||
)
|
||||
.and(Checkbox::new(default_is_outline, "Set Default to Outline"))
|
||||
.into_columns(),
|
||||
)
|
||||
.into_columns()
|
||||
// Run the button as an an application.
|
||||
.into_rows()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
// end rustme snippet
|
||||
|
|
|
|||
19
examples/checkbox.rs
Normal file
19
examples/checkbox.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::checkbox::CheckboxState;
|
||||
use gooey::widgets::Checkbox;
|
||||
use gooey::Run;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let checkbox_state = Dynamic::new(CheckboxState::Checked);
|
||||
let label = checkbox_state.map_each(|state| format!("Check Me! Current: {state:?}"));
|
||||
|
||||
Checkbox::new(checkbox_state.clone(), label)
|
||||
.and("Maybe".into_button().on_click(move |()| {
|
||||
checkbox_state.update(CheckboxState::Indeterminant);
|
||||
}))
|
||||
.into_columns()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use gooey::animation::ZeroToOne;
|
||||
use gooey::styles::components::{TextColor, WidgetBackground};
|
||||
use gooey::styles::{
|
||||
|
|
@ -87,26 +85,14 @@ fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) {
|
|||
)
|
||||
}
|
||||
|
||||
fn create_paired_string<T>(initial_value: T) -> (Dynamic<T>, Dynamic<String>)
|
||||
where
|
||||
T: ToString + PartialEq + FromStr + Default + Send + Sync + 'static,
|
||||
{
|
||||
let float = Dynamic::new(initial_value);
|
||||
let text = float.map_each_unique(|f| f.to_string());
|
||||
text.for_each(float.with_clone(|float| {
|
||||
move |text: &String| {
|
||||
let _result = float.try_update(text.parse().unwrap_or_default());
|
||||
}
|
||||
}));
|
||||
(float, text)
|
||||
}
|
||||
|
||||
fn color_editor(
|
||||
initial_color: ColorSource,
|
||||
label: &str,
|
||||
) -> (Dynamic<ColorSource>, impl MakeWidget) {
|
||||
let (hue, hue_text) = create_paired_string(initial_color.hue.into_degrees());
|
||||
let (saturation, saturation_text) = create_paired_string(initial_color.saturation);
|
||||
let hue = Dynamic::new(initial_color.hue.into_degrees());
|
||||
let hue_text = hue.linked_string();
|
||||
let saturation = Dynamic::new(initial_color.saturation);
|
||||
let saturation_text = saturation.linked_string();
|
||||
|
||||
let color =
|
||||
(&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation));
|
||||
|
|
@ -274,6 +260,7 @@ fn surface_theme(theme: Dynamic<SurfaceTheme>) -> impl MakeWidget {
|
|||
fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
|
||||
let color = theme.map_each(|theme| theme.color);
|
||||
let dim_color = theme.map_each(|theme| theme.color_dim);
|
||||
let bright_color = theme.map_each(|theme| theme.color_bright);
|
||||
let on_color = theme.map_each(|theme| theme.on_color);
|
||||
let container = theme.map_each(|theme| theme.container);
|
||||
let on_container = theme.map_each(|theme| theme.on_container);
|
||||
|
|
@ -284,6 +271,11 @@ fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
|
|||
&format!("{label} Dim"),
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
bright_color.clone(),
|
||||
&format!("{label} bright"),
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
on_color.clone(),
|
||||
&format!("On {label}"),
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
|
|||
/// Strokes an outline around this widget's contents.
|
||||
pub fn stroke_outline<Unit>(&mut self, color: Color, options: StrokeOptions<Unit>)
|
||||
where
|
||||
Unit: ScreenScale<Px = Px, Lp = Lp>,
|
||||
Unit: ScreenScale<Px = Px, Lp = Lp, UPx = UPx>,
|
||||
{
|
||||
let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1)));
|
||||
let focus_ring =
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use std::ops::Sub;
|
|||
pub use kludgine;
|
||||
use kludgine::app::winit::error::EventLoopError;
|
||||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{Fraction, IntoUnsigned, ScreenUnit};
|
||||
use kludgine::figures::{Fraction, ScreenUnit};
|
||||
pub use names::Name;
|
||||
pub use utils::{Lazy, WithClone};
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ impl ConstraintLimit {
|
|||
where
|
||||
Unit: ScreenUnit,
|
||||
{
|
||||
let measured = measured.into_px(scale).into_unsigned();
|
||||
let measured = measured.into_upx(scale);
|
||||
match self {
|
||||
ConstraintLimit::Known(size) => size.max(measured),
|
||||
ConstraintLimit::ClippedAfter(_) => measured,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use std::sync::Arc;
|
|||
|
||||
use ahash::AHashMap;
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{Fraction, IntoUnsigned, Rect, ScreenScale, Size};
|
||||
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Rect, ScreenScale, Size};
|
||||
use kludgine::Color;
|
||||
use palette::{IntoColor, Okhsl, OklabHue, Srgb};
|
||||
|
||||
|
|
@ -376,6 +376,7 @@ impl From<Lp> for Dimension {
|
|||
impl ScreenScale for Dimension {
|
||||
type Lp = Lp;
|
||||
type Px = Px;
|
||||
type UPx = UPx;
|
||||
|
||||
fn into_px(self, scale: kludgine::figures::Fraction) -> Px {
|
||||
match self {
|
||||
|
|
@ -398,6 +399,17 @@ impl ScreenScale for Dimension {
|
|||
fn from_lp(lp: Lp, _scale: kludgine::figures::Fraction) -> Self {
|
||||
Self::from(lp)
|
||||
}
|
||||
|
||||
fn into_upx(self, scale: Fraction) -> Self::UPx {
|
||||
match self {
|
||||
Dimension::Px(px) => px.into_unsigned(),
|
||||
Dimension::Lp(lp) => lp.into_upx(scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_upx(px: Self::UPx, _scale: Fraction) -> Self {
|
||||
Self::from(px.into_signed())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<i32> for Dimension {
|
||||
|
|
@ -469,10 +481,10 @@ impl DimensionRange {
|
|||
#[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());
|
||||
size = size.max(min.into_upx(scale));
|
||||
}
|
||||
if let Some(max) = self.maximum() {
|
||||
size = size.min(max.into_px(scale).into_unsigned());
|
||||
size = size.min(max.into_upx(scale));
|
||||
}
|
||||
size
|
||||
}
|
||||
|
|
@ -1152,6 +1164,8 @@ pub struct ColorTheme {
|
|||
pub color: Color,
|
||||
/// The primary color, dimmed for de-emphasized or disabled content.
|
||||
pub color_dim: Color,
|
||||
/// The primary color, brightened for highlighting content.
|
||||
pub color_bright: Color,
|
||||
/// The color for content that sits atop the primary color.
|
||||
pub on_color: Color,
|
||||
/// The backgrond color for containers.
|
||||
|
|
@ -1167,6 +1181,7 @@ impl ColorTheme {
|
|||
Self {
|
||||
color: source.color(40),
|
||||
color_dim: source.color(30),
|
||||
color_bright: source.color(45),
|
||||
on_color: source.color(100),
|
||||
container: source.color(90),
|
||||
on_container: source.color(10),
|
||||
|
|
@ -1179,6 +1194,7 @@ impl ColorTheme {
|
|||
Self {
|
||||
color: source.color(70),
|
||||
color_dim: source.color(60),
|
||||
color_bright: source.color(75),
|
||||
on_color: source.color(10),
|
||||
container: source.color(30),
|
||||
on_container: source.color(90),
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@ define_components! {
|
|||
SurfaceColor(Color, "surface_color", .surface.color)
|
||||
/// The [`Color`] to use when rendering text.
|
||||
TextColor(Color, "text_color", .surface.on_color)
|
||||
/// The [`Color`] to use when rendering text in a more subdued tone.
|
||||
TextColorVariant(Color, "text_color_variant", .surface.on_color_variant)
|
||||
/// A [`Color`] to be used as a highlight color.
|
||||
HighlightColor(Color,"highlight_color",.primary.color.with_alpha(128))
|
||||
/// Intrinsic, uniform padding for a widget.
|
||||
|
|
@ -122,6 +124,8 @@ define_components! {
|
|||
AutoFocusableControls(FocusableWidgets, "focus", FocusableWidgets::default())
|
||||
/// A [`Color`] to be used as the background color of a widget.
|
||||
WidgetBackground(Color, "widget_backgrond_color", Color::CLEAR_WHITE)
|
||||
/// A [`Color`] to be used to accent a widget.
|
||||
WidgetAccentColor(Color, "widget_accent_color", .primary.color)
|
||||
/// A [`Color`] to be used as an outline color.
|
||||
OutlineColor(Color, "outline_color", .surface.outline)
|
||||
/// A [`Color`] to be used as an outline color.
|
||||
|
|
|
|||
108
src/value.rs
108
src/value.rs
|
|
@ -5,6 +5,7 @@ use std::fmt::{Debug, Display};
|
|||
use std::future::Future;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, TryLockError};
|
||||
use std::task::{Poll, Waker};
|
||||
use std::thread::ThreadId;
|
||||
|
|
@ -43,6 +44,74 @@ impl<T> Dynamic<T> {
|
|||
}))
|
||||
}
|
||||
|
||||
/// Returns a new dynamic that has its contents linked with `self` by the
|
||||
/// pair of mapping functions provided.
|
||||
///
|
||||
/// When the returned dynamic is updated, `r_into_t` will be invoked. This
|
||||
/// function accepts `&R` and can return `T`, or `Option<T>`. If a value is
|
||||
/// produced, `self` will be updated with the new value.
|
||||
///
|
||||
/// When `self` is updated, `t_into_r` will be invoked. This function
|
||||
/// accepts `&T` and can return `R` or `Option<R>`. If a value is produced,
|
||||
/// the returned dynamic will be updated with the new value.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if calling `t_into_r` with the current contents of
|
||||
/// the Dynamic produces a `None` value. This requirement is only for the
|
||||
/// first invocation, and it is guaranteed to occur before this function
|
||||
/// returns.
|
||||
pub fn linked<R, TIntoR, TIntoRResult, RIntoT, RIntoTResult>(
|
||||
&self,
|
||||
mut t_into_r: TIntoR,
|
||||
mut r_into_t: RIntoT,
|
||||
) -> Dynamic<R>
|
||||
where
|
||||
T: PartialEq + Send + 'static,
|
||||
R: PartialEq + Send + 'static,
|
||||
TIntoRResult: Into<Option<R>> + Send + 'static,
|
||||
RIntoTResult: Into<Option<T>> + Send + 'static,
|
||||
TIntoR: FnMut(&T) -> TIntoRResult + Send + 'static,
|
||||
RIntoT: FnMut(&R) -> RIntoTResult + Send + 'static,
|
||||
{
|
||||
let initial_r = self
|
||||
.map_ref(&mut t_into_r)
|
||||
.into()
|
||||
.expect("t_into_r must succeed with the current value");
|
||||
let r = Dynamic::new(initial_r);
|
||||
r.with_clone(move |r| {
|
||||
self.for_each(move |t| {
|
||||
if let Some(update) = t_into_r(t).into() {
|
||||
let _result = r.try_update(update);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.with_clone(|t| {
|
||||
r.with_for_each(move |r| {
|
||||
if let Some(update) = r_into_t(r).into() {
|
||||
let _result = t.try_update(update);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a [linked](Self::linked) dynamic containing a `String`.
|
||||
///
|
||||
/// When `self` is updated, [`ToString::to_string()`] will be called to
|
||||
/// produce a new string value to store in the returned dynamic.
|
||||
///
|
||||
/// When the returned dynamic is updated, [`str::parse`](std::str) is called
|
||||
/// to produce a new `T`. If an error is returned, `self` will not be
|
||||
/// updated. Otherwise, `self` will be updated with the produced value.
|
||||
#[must_use]
|
||||
pub fn linked_string(&self) -> Dynamic<String>
|
||||
where
|
||||
T: ToString + FromStr + PartialEq + Send + 'static,
|
||||
{
|
||||
self.linked(ToString::to_string, |s: &String| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Maps the contents with read-only access.
|
||||
///
|
||||
/// # Panics
|
||||
|
|
@ -749,6 +818,45 @@ impl<T> DynamicReader<T> {
|
|||
value
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently contained value.
|
||||
///
|
||||
/// This function marks the currently stored value as being read.
|
||||
///
|
||||
/// `context` will be invalidated when the value is updated.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if this value is already locked by the current
|
||||
/// thread.
|
||||
#[must_use]
|
||||
pub fn get_tracking_refresh(&mut self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.source.redraw_when_changed(context.handle());
|
||||
self.get()
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently contained value.
|
||||
///
|
||||
/// This function marks the currently stored value as being read.
|
||||
///
|
||||
/// `context` will be invalidated when the value is updated.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if this value is already locked by the current
|
||||
/// thread.
|
||||
#[must_use]
|
||||
pub fn get_tracking_invalidate(&mut self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.source
|
||||
.invalidate_when_changed(context.handle(), context.widget().id());
|
||||
self.get()
|
||||
}
|
||||
|
||||
/// Blocks the current thread until the contained value has been updated or
|
||||
/// there are no remaining writers for the value.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -228,6 +228,18 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
|
|||
/// Returns the child widget.
|
||||
fn child_mut(&mut self) -> &mut WidgetRef;
|
||||
|
||||
/// Draws the background of the widget.
|
||||
///
|
||||
/// This is invoked before the wrapped widget is drawn.
|
||||
#[allow(unused_variables)]
|
||||
fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {}
|
||||
|
||||
/// Draws the foreground of the widget.
|
||||
///
|
||||
/// This is invoked after the wrapped widget is drawn.
|
||||
#[allow(unused_variables)]
|
||||
fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {}
|
||||
|
||||
/// Returns the rectangle that the child widget should occupy given
|
||||
/// `available_space`.
|
||||
#[allow(unused_variables)]
|
||||
|
|
@ -266,7 +278,7 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> WrappedLayout {
|
||||
Size::<UPx>::new(
|
||||
Size::new(
|
||||
available_space
|
||||
.width
|
||||
.fit_measured(size.width, context.gfx.scale()),
|
||||
|
|
@ -419,8 +431,12 @@ where
|
|||
context.gfx.fill(color);
|
||||
}
|
||||
|
||||
self.redraw_background(context);
|
||||
|
||||
let child = self.child_mut().mounted(&mut context.as_event_context());
|
||||
context.for_other(&child).redraw();
|
||||
|
||||
self.redraw_foreground(context);
|
||||
}
|
||||
|
||||
fn layout(
|
||||
|
|
@ -627,9 +643,12 @@ pub trait MakeWidget: Sized {
|
|||
|
||||
/// Resizes `self` to `width`.
|
||||
///
|
||||
/// `width` can be an individual
|
||||
/// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a
|
||||
/// range.
|
||||
/// `width` can be an any of:
|
||||
///
|
||||
/// - [`Dimension`]
|
||||
/// - [`Px`]
|
||||
/// - [`Lp`](crate::kludgine::figures::units::Lp)
|
||||
/// - A range of any fo the above.
|
||||
#[must_use]
|
||||
fn width(self, width: impl Into<DimensionRange>) -> Resize {
|
||||
Resize::from_width(width, self)
|
||||
|
|
@ -637,9 +656,12 @@ pub trait MakeWidget: Sized {
|
|||
|
||||
/// Resizes `self` to `height`.
|
||||
///
|
||||
/// `height` can be an individual
|
||||
/// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a
|
||||
/// range.
|
||||
/// `height` can be an any of:
|
||||
///
|
||||
/// - [`Dimension`]
|
||||
/// - [`Px`]
|
||||
/// - [`Lp`](crate::kludgine::figures::units::Lp)
|
||||
/// - A range of any fo the above.
|
||||
#[must_use]
|
||||
fn height(self, height: impl Into<DimensionRange>) -> Resize {
|
||||
Resize::from_height(height, self)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
mod align;
|
||||
pub mod button;
|
||||
mod canvas;
|
||||
pub mod checkbox;
|
||||
pub mod container;
|
||||
mod expand;
|
||||
mod input;
|
||||
|
|
@ -21,6 +22,7 @@ mod tilemap;
|
|||
pub use align::Align;
|
||||
pub use button::Button;
|
||||
pub use canvas::Canvas;
|
||||
pub use checkbox::Checkbox;
|
||||
pub use container::Container;
|
||||
pub use expand::Expand;
|
||||
pub use input::Input;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::figures::{Fraction, IntoSigned, Point, Rect, ScreenScale, Size};
|
||||
|
||||
use crate::context::{AsEventContext, LayoutContext};
|
||||
use crate::styles::{Edges, FlexibleDimension};
|
||||
|
|
@ -125,15 +125,11 @@ 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())
|
||||
}
|
||||
FlexibleDimension::Dimension(dimension) => Some(dimension.into_upx(scale)),
|
||||
};
|
||||
let b = match b {
|
||||
FlexibleDimension::Auto => None,
|
||||
FlexibleDimension::Dimension(dimension) => {
|
||||
Some(dimension.into_px(scale).into_unsigned())
|
||||
}
|
||||
FlexibleDimension::Dimension(dimension) => Some(dimension.into_upx(scale)),
|
||||
};
|
||||
Self { a, b }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@ use std::time::Duration;
|
|||
|
||||
use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::shapes::StrokeOptions;
|
||||
use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::animation::{AnimationHandle, AnimationTarget, LinearInterpolate, Spawn};
|
||||
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::styles::components::{
|
||||
AutoFocusableControls, Easing, IntrinsicPadding, OpaqueWidgetColor, SurfaceColor, TextColor,
|
||||
AutoFocusableControls, Easing, HighlightColor, IntrinsicPadding, OpaqueWidgetColor,
|
||||
OutlineColor, SurfaceColor, TextColor,
|
||||
};
|
||||
use crate::styles::Styles;
|
||||
use crate::styles::{ColorExt, Styles};
|
||||
use crate::utils::ModifiersExt;
|
||||
use crate::value::{Dynamic, IntoValue, Value};
|
||||
use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED, IGNORED};
|
||||
|
|
@ -27,17 +28,100 @@ pub struct Button {
|
|||
pub on_click: Option<Callback<()>>,
|
||||
/// The enabled state of the button.
|
||||
pub enabled: Value<bool>,
|
||||
currently_enabled: bool,
|
||||
/// The kind of button to draw.
|
||||
pub kind: Value<ButtonKind>,
|
||||
buttons_pressed: usize,
|
||||
active_style: Option<Dynamic<ButtonStyle>>,
|
||||
cached_state: CacheState,
|
||||
active_colors: Option<Dynamic<ButtonColors>>,
|
||||
color_animation: AnimationHandle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
struct CacheState {
|
||||
enabled: bool,
|
||||
kind: ButtonKind,
|
||||
}
|
||||
|
||||
/// The type of a [`Button`] or similar clickable widget.
|
||||
#[derive(Debug, Default, Eq, PartialEq, Clone, Copy)]
|
||||
pub enum ButtonKind {
|
||||
/// A solid button.
|
||||
#[default]
|
||||
Solid,
|
||||
/// An outline button, which uses the same colors as [`ButtonKind::Solid`]
|
||||
/// but swaps the outline and background colors.
|
||||
Outline,
|
||||
/// A transparent button, which is transparent until it is hovered.
|
||||
Transparent,
|
||||
}
|
||||
|
||||
impl ButtonKind {
|
||||
/// Returns the [`ButtonColors`] to apply for a
|
||||
/// [default](MakeWidget::into_default) button.
|
||||
#[must_use]
|
||||
pub fn colors_for_default(
|
||||
self,
|
||||
visual_state: VisualState,
|
||||
context: &WidgetContext<'_, '_>,
|
||||
) -> ButtonColors {
|
||||
match self {
|
||||
ButtonKind::Solid => match visual_state {
|
||||
VisualState::Normal => ButtonColors {
|
||||
background: context.theme().primary.color,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.get(&ButtonOutline),
|
||||
},
|
||||
VisualState::Hovered => ButtonColors {
|
||||
background: context.theme().primary.color_bright,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.get(&ButtonHoverOutline),
|
||||
},
|
||||
VisualState::Active => ButtonColors {
|
||||
background: context.theme().primary.color_dim,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.get(&ButtonActiveOutline),
|
||||
},
|
||||
VisualState::Disabled => ButtonColors {
|
||||
background: context.theme().primary.color_dim,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.get(&ButtonDisabledOutline),
|
||||
},
|
||||
},
|
||||
ButtonKind::Outline | ButtonKind::Transparent => match visual_state {
|
||||
VisualState::Normal => ButtonColors {
|
||||
background: context.get(&ButtonOutline),
|
||||
foreground: context.theme().primary.color,
|
||||
outline: context.theme().primary.color,
|
||||
},
|
||||
VisualState::Hovered => ButtonColors {
|
||||
background: context.get(&ButtonHoverOutline),
|
||||
foreground: context.theme().primary.color,
|
||||
outline: context.theme().primary.color_bright,
|
||||
},
|
||||
VisualState::Active => ButtonColors {
|
||||
background: context.get(&ButtonActiveOutline),
|
||||
foreground: context.theme().primary.color,
|
||||
outline: context.theme().surface.color,
|
||||
},
|
||||
VisualState::Disabled => ButtonColors {
|
||||
background: context.get(&ButtonDisabledOutline),
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.theme().primary.color_dim,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The coloring to apply to a [`Button`] or button-like widget.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, LinearInterpolate)]
|
||||
struct ButtonStyle {
|
||||
background: Color,
|
||||
foreground: Color,
|
||||
outline: Color,
|
||||
pub struct ButtonColors {
|
||||
/// The background color of the button.
|
||||
pub background: Color,
|
||||
/// The foreground (text) color of the button.
|
||||
pub foreground: Color,
|
||||
/// A color to use to surround the button.
|
||||
pub outline: Color,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
|
|
@ -47,13 +131,24 @@ impl Button {
|
|||
content: content.widget_ref(),
|
||||
on_click: None,
|
||||
enabled: Value::Constant(true),
|
||||
currently_enabled: true,
|
||||
cached_state: CacheState {
|
||||
enabled: true,
|
||||
kind: ButtonKind::default(),
|
||||
},
|
||||
buttons_pressed: 0,
|
||||
active_style: None,
|
||||
active_colors: None,
|
||||
kind: Value::Constant(ButtonKind::default()),
|
||||
color_animation: AnimationHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the button's `kind` and returns self.
|
||||
#[must_use]
|
||||
pub fn kind(mut self, kind: impl IntoValue<ButtonKind>) -> Self {
|
||||
self.kind = kind.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `on_click` callback and returns self.
|
||||
///
|
||||
/// This callback will be invoked each time the button is clicked.
|
||||
|
|
@ -70,7 +165,6 @@ impl Button {
|
|||
#[must_use]
|
||||
pub fn enabled(mut self, enabled: impl IntoValue<bool>) -> Self {
|
||||
self.enabled = enabled.into_value();
|
||||
self.currently_enabled = self.enabled.get();
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -82,37 +176,72 @@ impl Button {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_colors(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) {
|
||||
let new_style = match () {
|
||||
() if !self.enabled.get() => ButtonStyle {
|
||||
background: context.get(&ButtonDisabledBackground),
|
||||
foreground: context.get(&ButtonDisabledForeground),
|
||||
outline: context.get(&ButtonDisabledOutline),
|
||||
fn visual_style(&self, context: &WidgetContext<'_, '_>) -> VisualState {
|
||||
if !self.enabled.get_tracked(context) {
|
||||
VisualState::Disabled
|
||||
} else if context.active() {
|
||||
VisualState::Active
|
||||
} else if context.hovered() {
|
||||
VisualState::Hovered
|
||||
} else {
|
||||
VisualState::Normal
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the coloring to apply to a [`ButtonKind::Transparent`] button.
|
||||
#[must_use]
|
||||
pub fn colors_for_transparent(
|
||||
visual_state: VisualState,
|
||||
context: &WidgetContext<'_, '_>,
|
||||
) -> ButtonColors {
|
||||
match visual_state {
|
||||
VisualState::Normal => ButtonColors {
|
||||
background: Color::CLEAR_BLACK,
|
||||
foreground: context.get(&TextColor),
|
||||
outline: context.get(&ButtonOutline),
|
||||
},
|
||||
// TODO this probably should use actual style.
|
||||
() if context.is_default() => ButtonStyle {
|
||||
background: context.theme().primary.color,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: Color::CLEAR_BLACK,
|
||||
VisualState::Hovered => ButtonColors {
|
||||
background: context.get(&OpaqueWidgetColor),
|
||||
foreground: context.get(&TextColor),
|
||||
outline: context.get(&ButtonHoverOutline),
|
||||
},
|
||||
() if context.active() => ButtonStyle {
|
||||
VisualState::Active => ButtonColors {
|
||||
background: context.get(&ButtonActiveBackground),
|
||||
foreground: context.get(&ButtonActiveForeground),
|
||||
outline: context.get(&ButtonActiveOutline),
|
||||
},
|
||||
() if context.hovered() => ButtonStyle {
|
||||
background: context.get(&ButtonHoverBackground),
|
||||
foreground: context.get(&ButtonHoverForeground),
|
||||
outline: context.get(&ButtonHoverOutline),
|
||||
},
|
||||
() => ButtonStyle {
|
||||
background: context.get(&ButtonBackground),
|
||||
foreground: context.get(&ButtonForeground),
|
||||
outline: context.get(&ButtonOutline),
|
||||
VisualState::Disabled => ButtonColors {
|
||||
background: Color::CLEAR_BLACK,
|
||||
foreground: context.theme().surface.on_color_variant,
|
||||
outline: context.get(&ButtonDisabledOutline),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_stateful_colors(&mut self, context: &WidgetContext<'_, '_>) -> ButtonColors {
|
||||
let kind = self.kind.get_tracked(context);
|
||||
let visual_state = self.visual_style(context);
|
||||
|
||||
self.cached_state = CacheState {
|
||||
enabled: !matches!(visual_state, VisualState::Disabled),
|
||||
kind,
|
||||
};
|
||||
|
||||
match (immediate, &self.active_style) {
|
||||
if context.is_default() {
|
||||
kind.colors_for_default(visual_state, context)
|
||||
} else {
|
||||
match kind {
|
||||
ButtonKind::Transparent => Self::colors_for_transparent(visual_state, context),
|
||||
ButtonKind::Solid => visual_state.solid_colors(context),
|
||||
ButtonKind::Outline => visual_state.outline_colors(context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_colors(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) {
|
||||
let new_style = self.determine_stateful_colors(context);
|
||||
|
||||
match (immediate, &self.active_colors) {
|
||||
(false, Some(style)) => {
|
||||
self.color_animation = (style.transition_to(new_style))
|
||||
.over(Duration::from_millis(150))
|
||||
|
|
@ -126,43 +255,139 @@ impl Button {
|
|||
_ => {
|
||||
let new_style = Dynamic::new(new_style);
|
||||
let foreground = new_style.map_each(|s| s.foreground);
|
||||
self.active_style = Some(new_style);
|
||||
self.active_colors = Some(new_style);
|
||||
context.attach_styles(Styles::new().with(&TextColor, foreground));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_style(&mut self, context: &WidgetContext<'_, '_>) -> ButtonStyle {
|
||||
if self.active_style.is_none() {
|
||||
fn current_style(&mut self, context: &WidgetContext<'_, '_>) -> ButtonColors {
|
||||
if self.active_colors.is_none() {
|
||||
self.update_colors(context, false);
|
||||
}
|
||||
|
||||
let style = self.active_style.as_ref().expect("always initialized");
|
||||
let style = self.active_colors.as_ref().expect("always initialized");
|
||||
context.redraw_when_changed(style);
|
||||
style.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// The effective visual state of an element.
|
||||
///
|
||||
/// While an element may be multiple states (e.g., active and hovered), when
|
||||
/// rendering a widget sometimes a single visual style must take priority. This
|
||||
/// enum represents the various states a widget may be in for such a decision.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum VisualState {
|
||||
/// The widget should render in its normal state.
|
||||
Normal,
|
||||
/// The widget should render in reaction to the mouse cursor being above the
|
||||
/// widget.
|
||||
Hovered,
|
||||
/// The widget should render in reaction to the widget being clicked on or
|
||||
/// activated by the user.
|
||||
Active,
|
||||
/// The widget should render in a way to convey to the user it is not
|
||||
/// enabled for interaction.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl VisualState {
|
||||
/// Returns the colors to apply to a [`ButtonKind::Solid`] [`Button`] or
|
||||
/// button-like widget.
|
||||
#[must_use]
|
||||
pub fn solid_colors(self, context: &WidgetContext<'_, '_>) -> ButtonColors {
|
||||
match self {
|
||||
VisualState::Normal => ButtonColors {
|
||||
background: context.get(&ButtonBackground),
|
||||
foreground: context.get(&ButtonForeground),
|
||||
outline: context.get(&ButtonOutline),
|
||||
},
|
||||
VisualState::Hovered => ButtonColors {
|
||||
background: context.get(&ButtonHoverBackground),
|
||||
foreground: context.get(&ButtonHoverForeground),
|
||||
outline: context.get(&ButtonHoverOutline),
|
||||
},
|
||||
VisualState::Active => ButtonColors {
|
||||
background: context.get(&ButtonActiveBackground),
|
||||
foreground: context.get(&ButtonActiveForeground),
|
||||
outline: context.get(&ButtonActiveOutline),
|
||||
},
|
||||
VisualState::Disabled => ButtonColors {
|
||||
background: context.get(&ButtonDisabledBackground),
|
||||
foreground: context.get(&ButtonDisabledForeground),
|
||||
outline: context.get(&ButtonDisabledOutline),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the colors to apply to a [`ButtonKind::Outline`] [`Button`] or
|
||||
/// button-like widget.
|
||||
#[must_use]
|
||||
pub fn outline_colors(self, context: &WidgetContext<'_, '_>) -> ButtonColors {
|
||||
let solid = self.solid_colors(context);
|
||||
ButtonColors {
|
||||
background: solid.outline,
|
||||
foreground: solid.foreground,
|
||||
outline: solid.background,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Button {
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
#![allow(clippy::similar_names)]
|
||||
let enabled = self.enabled.get();
|
||||
let enabled = self.enabled.get_tracked(context);
|
||||
|
||||
// TODO This seems ugly. It needs context, so it can't be moved into the
|
||||
// dynamic system.
|
||||
if self.currently_enabled != enabled {
|
||||
let current_style = self.kind.get_tracked(context);
|
||||
if self.cached_state.enabled != enabled || self.cached_state.kind != current_style {
|
||||
self.update_colors(context, false);
|
||||
self.currently_enabled = enabled;
|
||||
}
|
||||
|
||||
self.enabled.redraw_when_changed(context);
|
||||
|
||||
let style = self.current_style(context);
|
||||
context.gfx.fill(style.background);
|
||||
|
||||
context.stroke_outline::<Lp>(style.outline, StrokeOptions::default());
|
||||
let two_lp_stroke = StrokeOptions::lp_wide(Lp::points(2));
|
||||
context.stroke_outline(style.outline, two_lp_stroke);
|
||||
|
||||
if context.focused() {
|
||||
context.draw_focus_ring();
|
||||
if current_style == ButtonKind::Transparent {
|
||||
let two_lp_stroke = two_lp_stroke.into_px(context.gfx.scale());
|
||||
let focus_color = context.get(&HighlightColor);
|
||||
// Some states of a transparent button have solid background
|
||||
// colors. most_contrasting from a 0-alpha color is not a
|
||||
// meaningful measurement, so we only start measuring contrast
|
||||
// once we reach 50% opacity. If we ever add solid background
|
||||
// tracking (<https://github.com/khonsulabs/gooey/issues/73>),
|
||||
// we should use that color for most_contrasting always.
|
||||
let color = if style.background.alpha() > 128 {
|
||||
style
|
||||
.background
|
||||
.most_contrasting(&[focus_color, context.get(&TextColor)])
|
||||
} else {
|
||||
focus_color
|
||||
};
|
||||
|
||||
let inset = context.get(&IntrinsicPadding).into_px(context.gfx.scale());
|
||||
|
||||
let focus_ring = Shape::stroked_rect(
|
||||
Rect::new(
|
||||
Point::new(inset, inset),
|
||||
context.gfx.region().size - inset * 2,
|
||||
),
|
||||
color,
|
||||
two_lp_stroke,
|
||||
);
|
||||
context
|
||||
.gfx
|
||||
.draw_shape(&focus_ring, Point::default(), None, None);
|
||||
} else if context.is_default() {
|
||||
context.stroke_outline(context.get(&OutlineColor), two_lp_stroke);
|
||||
} else {
|
||||
context.draw_focus_ring();
|
||||
}
|
||||
}
|
||||
|
||||
let content = self.content.mounted(&mut context.as_event_context());
|
||||
|
|
@ -237,10 +462,7 @@ impl Widget for Button {
|
|||
available_space: Size<crate::ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let padding = context
|
||||
.get(&IntrinsicPadding)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
|
||||
let mounted = self.content.mounted(&mut context.as_event_context());
|
||||
let size = context.for_other(&mounted).layout(available_space);
|
||||
context.set_child_layout(
|
||||
|
|
@ -317,7 +539,7 @@ define_components! {
|
|||
ButtonActiveBackground(Color, "active_background_color", .surface.color)
|
||||
/// The background color of the button when the mouse cursor is hovering over
|
||||
/// it.
|
||||
ButtonHoverBackground(Color, "hover_background_color", .surface.bright_color)
|
||||
ButtonHoverBackground(Color, "hover_background_color", .surface.lowest_container)
|
||||
/// The background color of the button when the mouse cursor is hovering over
|
||||
/// it.
|
||||
ButtonDisabledBackground(Color, "disabled_background_color", .surface.dim_color)
|
||||
|
|
@ -334,12 +556,12 @@ define_components! {
|
|||
/// The outline color of the button.
|
||||
ButtonOutline(Color, "outline_color", Color::CLEAR_BLACK)
|
||||
/// The outline color of the button when it is active (depressed).
|
||||
ButtonActiveOutline(Color, "active_outline_color", contrasting!(ButtonActiveBackground, ButtonOutline, TextColor, SurfaceColor))
|
||||
ButtonActiveOutline(Color, "active_outline_color", Color::CLEAR_BLACK)
|
||||
/// The outline color of the button when the mouse cursor is hovering over
|
||||
/// it.
|
||||
ButtonHoverOutline(Color, "hover_outline_color", contrasting!(ButtonHoverBackground, ButtonOutline, TextColor, SurfaceColor))
|
||||
ButtonHoverOutline(Color, "hover_outline_color", Color::CLEAR_BLACK)
|
||||
/// The outline color of the button when the mouse cursor is hovering over
|
||||
/// it.
|
||||
ButtonDisabledOutline(Color, "disabled_outline_color", contrasting!(ButtonDisabledBackground, ButtonOutline, TextColor, SurfaceColor))
|
||||
ButtonDisabledOutline(Color, "disabled_outline_color", Color::CLEAR_BLACK)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
233
src/widgets/checkbox.rs
Normal file
233
src/widgets/checkbox.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
//! A tri-state, labelable checkbox widget.
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use std::ops::Not;
|
||||
|
||||
use kludgine::figures::units::{Lp, Px};
|
||||
use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::shapes::{PathBuilder, Shape, StrokeOptions};
|
||||
|
||||
use crate::context::{GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::{
|
||||
IntrinsicPadding, LineHeight, OutlineColor, TextColor, WidgetAccentColor,
|
||||
};
|
||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, WidgetInstance, WidgetRef, WrappedLayout, WrapperWidget};
|
||||
use crate::widgets::button::ButtonKind;
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A labeled-widget that supports three states: Checked, Unchecked, and
|
||||
/// Indeterminant
|
||||
pub struct Checkbox {
|
||||
/// The state (value) of the checkbox.
|
||||
pub state: Dynamic<CheckboxState>,
|
||||
/// The button kind to use as the basis for this checkbox. Checkboxes
|
||||
/// default to [`ButtonKind::Transparent`].
|
||||
pub kind: Value<ButtonKind>,
|
||||
label: WidgetInstance,
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
/// Returns a new checkbox that updates `state` when clicked. `label` is
|
||||
/// drawn next to the checkbox and is also clickable to toggle the checkbox.
|
||||
///
|
||||
/// `state` can also be a `Dynamic<bool>` if there is no need to represent
|
||||
/// an indeterminant state.
|
||||
pub fn new(state: impl IntoDynamic<CheckboxState>, label: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
state: state.into_dynamic(),
|
||||
kind: Value::Constant(ButtonKind::Transparent),
|
||||
label: label.make_widget(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the button kind to use as the basis for this checkbox, and
|
||||
/// returns self.
|
||||
///
|
||||
/// Checkboxes default to [`ButtonKind::Transparent`].
|
||||
#[must_use]
|
||||
pub fn kind(mut self, kind: impl IntoValue<ButtonKind>) -> Self {
|
||||
self.kind = kind.into_value();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MakeWidget for Checkbox {
|
||||
fn make_widget(self) -> WidgetInstance {
|
||||
CheckboxLabel {
|
||||
value: self.state.create_reader(),
|
||||
label: WidgetRef::new(self.label),
|
||||
}
|
||||
.into_button()
|
||||
.on_click(move |()| {
|
||||
let mut value = self.state.lock();
|
||||
*value = !*value;
|
||||
})
|
||||
.kind(self.kind)
|
||||
.make_widget()
|
||||
}
|
||||
}
|
||||
|
||||
/// The state/value of a [`Checkbox`].
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum CheckboxState {
|
||||
/// The checkbox should display showing that it is neither checked or
|
||||
/// unchecked.
|
||||
///
|
||||
/// This state is used to represent concepts such as:
|
||||
///
|
||||
/// - States that are neither true/false, or on/off.
|
||||
/// - States that are partially true or partially on.
|
||||
Indeterminant,
|
||||
/// The checkbox should display in an unchecked/off/false state.
|
||||
Unchecked,
|
||||
/// The checkbox should display in an checked/on/true state.
|
||||
Checked,
|
||||
}
|
||||
|
||||
impl From<bool> for CheckboxState {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Self::Checked
|
||||
} else {
|
||||
Self::Unchecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CheckboxState> for bool {
|
||||
type Error = CheckboxToBoolError;
|
||||
|
||||
fn try_from(value: CheckboxState) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
CheckboxState::Checked => Ok(true),
|
||||
CheckboxState::Unchecked => Ok(false),
|
||||
CheckboxState::Indeterminant => Err(CheckboxToBoolError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Not for CheckboxState {
|
||||
type Output = Self;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
match self {
|
||||
Self::Indeterminant | Self::Unchecked => Self::Checked,
|
||||
Self::Checked => Self::Unchecked,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDynamic<CheckboxState> for Dynamic<bool> {
|
||||
fn into_dynamic(self) -> Dynamic<CheckboxState> {
|
||||
self.linked(
|
||||
|bool| CheckboxState::from(*bool),
|
||||
|tri_state: &CheckboxState| bool::try_from(*tri_state).ok(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`CheckboxState::Indeterminant`] was encountered when converting to a
|
||||
/// `bool`.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct CheckboxToBoolError;
|
||||
|
||||
impl Display for CheckboxToBoolError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("CheckboxState was Indeterminant")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for CheckboxToBoolError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CheckboxLabel {
|
||||
value: DynamicReader<CheckboxState>,
|
||||
label: WidgetRef,
|
||||
}
|
||||
|
||||
impl WrapperWidget for CheckboxLabel {
|
||||
fn child_mut(&mut self) -> &mut WidgetRef {
|
||||
&mut self.label
|
||||
}
|
||||
|
||||
fn position_child(
|
||||
&mut self,
|
||||
size: Size<Px>,
|
||||
_available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> WrappedLayout {
|
||||
let checkbox_size = context.get(&LineHeight).into_px(context.gfx.scale()); // TODO create a component?
|
||||
let padding = context.get(&IntrinsicPadding).into_px(context.gfx.scale());
|
||||
let label_inset = checkbox_size + padding;
|
||||
let size_with_checkbox = Size::new(size.width + label_inset, size.height).into_unsigned();
|
||||
WrappedLayout {
|
||||
child: Rect::new(Point::new(label_inset, Px(0)), size),
|
||||
size: size_with_checkbox,
|
||||
}
|
||||
}
|
||||
|
||||
fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let checkbox_size = context.get(&LineHeight).into_px(context.gfx.scale());
|
||||
let padding = context.get(&IntrinsicPadding).into_px(context.gfx.scale());
|
||||
let checkbox_rect = Rect::new(
|
||||
Point::new(padding, padding),
|
||||
Size::new(checkbox_size, checkbox_size),
|
||||
);
|
||||
let stroke_options = StrokeOptions::lp_wide(Lp::points(2)).into_px(context.gfx.scale());
|
||||
match self.value.get_tracking_refresh(context) {
|
||||
state @ (CheckboxState::Checked | CheckboxState::Indeterminant) => {
|
||||
let color = context.get(&WidgetAccentColor);
|
||||
context.gfx.draw_shape(
|
||||
&Shape::filled_rect(checkbox_rect, color),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let icon_area = checkbox_rect.inset(Lp::points(3).into_px(context.gfx.scale()));
|
||||
let text_color = context.get(&TextColor);
|
||||
let center = icon_area.origin + icon_area.size / 2;
|
||||
if matches!(state, CheckboxState::Checked) {
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new(Point::new(icon_area.origin.x, center.y))
|
||||
.line_to(Point::new(
|
||||
icon_area.origin.x + icon_area.size.width / 4,
|
||||
icon_area.origin.y + icon_area.size.height * 3 / 4,
|
||||
))
|
||||
.line_to(Point::new(
|
||||
icon_area.origin.x + icon_area.size.width,
|
||||
icon_area.origin.y,
|
||||
))
|
||||
.build()
|
||||
.stroke(text_color, stroke_options),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new(Point::new(icon_area.origin.x, center.y))
|
||||
.line_to(Point::new(
|
||||
icon_area.origin.x + icon_area.size.width,
|
||||
center.y,
|
||||
))
|
||||
.build()
|
||||
.stroke(text_color, stroke_options),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
CheckboxState::Unchecked => {
|
||||
let color = context.get(&OutlineColor);
|
||||
context.gfx.draw_shape(
|
||||
&Shape::stroked_rect(checkbox_rect, color, stroke_options),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -193,11 +193,7 @@ impl WrapperWidget for Container {
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<ConstraintLimit> {
|
||||
let padding_amount = self
|
||||
.padding(context)
|
||||
.size()
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let padding_amount = self.padding(context).size().into_upx(context.gfx.scale());
|
||||
Size::new(
|
||||
available_space.width - padding_amount.width,
|
||||
available_space.height - padding_amount.height,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{IntoSigned, Size};
|
||||
|
||||
use crate::context::{AsEventContext, LayoutContext};
|
||||
|
|
@ -133,6 +132,6 @@ impl WrapperWidget for Expand {
|
|||
),
|
||||
};
|
||||
|
||||
Size::<UPx>::new(width, height).into_signed().into()
|
||||
Size::new(width, height).into_signed().into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -351,10 +351,7 @@ impl Widget for Input {
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let padding = context
|
||||
.get(&IntrinsicPadding)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
|
||||
if self.needs_to_select_all {
|
||||
self.needs_to_select_all = false;
|
||||
self.select_all();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! A read-only text widget.
|
||||
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{IntoUnsigned, Point, ScreenScale, Size};
|
||||
use kludgine::figures::{Point, ScreenScale, Size};
|
||||
use kludgine::text::{MeasuredText, Text, TextOrigin};
|
||||
use kludgine::Color;
|
||||
|
||||
|
|
@ -79,10 +79,7 @@ impl Widget for Label {
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let padding = context
|
||||
.get(&IntrinsicPadding)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
|
||||
let color = context.get(&TextColor);
|
||||
let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
|
||||
let prepared = self.prepared_text(context, color, width);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, ScreenScale, Size};
|
||||
use kludgine::figures::{Fraction, IntoSigned, ScreenScale, Size};
|
||||
|
||||
use crate::context::{AsEventContext, LayoutContext};
|
||||
use crate::styles::DimensionRange;
|
||||
|
|
@ -48,9 +47,12 @@ impl Resize {
|
|||
|
||||
/// Resizes `self` to `width`.
|
||||
///
|
||||
/// `width` can be an individual
|
||||
/// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a
|
||||
/// range.
|
||||
/// `width` can be an any of:
|
||||
///
|
||||
/// - [`Dimension`](crate::styles::Dimension)
|
||||
/// - [`Px`](crate::kludgine::figures::units::Px)
|
||||
/// - [`Lp`](crate::kludgine::figures::units::Lp)
|
||||
/// - A range of any fo the above.
|
||||
#[must_use]
|
||||
pub fn width(mut self, width: impl Into<DimensionRange>) -> Self {
|
||||
self.width = width.into();
|
||||
|
|
@ -59,9 +61,12 @@ impl Resize {
|
|||
|
||||
/// Resizes `self` to `height`.
|
||||
///
|
||||
/// `width` can be an individual
|
||||
/// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a
|
||||
/// range.
|
||||
/// `height` can be an any of:
|
||||
///
|
||||
/// - [`Dimension`](crate::styles::Dimension)
|
||||
/// - [`Px`](crate::kludgine::figures::units::Px)
|
||||
/// - [`Lp`](crate::kludgine::figures::units::Lp)
|
||||
/// - A range of any fo the above.
|
||||
#[must_use]
|
||||
pub fn height(mut self, height: impl Into<DimensionRange>) -> Self {
|
||||
self.height = height.into();
|
||||
|
|
@ -94,8 +99,8 @@ impl WrapperWidget for Resize {
|
|||
(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(),
|
||||
width.into_upx(context.gfx.scale()),
|
||||
height.into_upx(context.gfx.scale()),
|
||||
)
|
||||
} else {
|
||||
let available_space = Size::new(
|
||||
|
|
@ -104,7 +109,7 @@ impl WrapperWidget for Resize {
|
|||
);
|
||||
context.for_other(&child).layout(available_space)
|
||||
};
|
||||
Size::<UPx>::new(
|
||||
Size::new(
|
||||
self.width.clamp(size.width, context.gfx.scale()),
|
||||
self.height.clamp(size.height, context.gfx.scale()),
|
||||
)
|
||||
|
|
@ -121,9 +126,7 @@ fn override_constraint(
|
|||
match constraint {
|
||||
ConstraintLimit::Known(size) => ConstraintLimit::Known(range.clamp(size, scale)),
|
||||
ConstraintLimit::ClippedAfter(clipped_after) => match (range.minimum(), range.maximum()) {
|
||||
(Some(min), Some(max)) if min == max => {
|
||||
ConstraintLimit::Known(min.into_px(scale).into_unsigned())
|
||||
}
|
||||
(Some(min), Some(max)) if min == max => ConstraintLimit::Known(min.into_upx(scale)),
|
||||
_ => ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale)),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,8 +172,7 @@ impl Widget for Scroll {
|
|||
let (mut scroll, current_max_scroll) = self.constrain_scroll();
|
||||
|
||||
let control_size =
|
||||
Size::<UPx>::new(available_space.width.max(), available_space.height.max())
|
||||
.into_signed();
|
||||
Size::new(available_space.width.max(), available_space.height.max()).into_signed();
|
||||
let max_extents = Size::new(
|
||||
if self.enabled.x {
|
||||
ConstraintLimit::ClippedAfter((control_size.width).into_unsigned())
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ use std::panic::UnwindSafe;
|
|||
use kludgine::app::winit::event::{DeviceId, MouseButton};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{
|
||||
FloatConversion, FromComponents, IntoComponents, IntoSigned, IntoUnsigned, Point, Ranged, Rect,
|
||||
ScreenScale, Size,
|
||||
FloatConversion, FromComponents, IntoComponents, IntoSigned, Point, Ranged, Rect, ScreenScale,
|
||||
Size,
|
||||
};
|
||||
use kludgine::shapes::Shape;
|
||||
use kludgine::{Color, Origin};
|
||||
|
||||
use crate::animation::{LinearInterpolate, PercentBetween};
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::OpaqueWidgetColor;
|
||||
use crate::styles::components::{OpaqueWidgetColor, WidgetAccentColor};
|
||||
use crate::styles::Dimension;
|
||||
use crate::value::{Dynamic, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{EventHandling, Widget, HANDLED};
|
||||
|
|
@ -223,14 +223,10 @@ where
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
self.knob_size = context
|
||||
.get(&KnobSize)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
self.knob_size = context.get(&KnobSize).into_upx(context.gfx.scale());
|
||||
let minimum_size = context
|
||||
.get(&MinimumSliderSize)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
.into_upx(context.gfx.scale());
|
||||
|
||||
match (available_space.width, available_space.height) {
|
||||
(ConstraintLimit::Known(width), ConstraintLimit::Known(height)) => {
|
||||
|
|
@ -321,7 +317,7 @@ define_components! {
|
|||
/// The minimum length of the slidable dimension.
|
||||
MinimumSliderSize(Dimension, "minimum_size", |context| context.get(&KnobSize) * 2)
|
||||
/// The color of the draggable portion of the knob.
|
||||
KnobColor(Color, "knob_color", .primary.color) // TODO make this pull from a component multiple widgets can share
|
||||
KnobColor(Color, "knob_color", @WidgetAccentColor)
|
||||
/// The color of the track that the knob rests on.
|
||||
TrackColor(Color,"track_color", |context| context.get(&KnobColor))
|
||||
/// The color of the track that the knob rests on.
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ impl Layout {
|
|||
Dimension::Px(size) => self.allocated_space.0 += size.into_unsigned(),
|
||||
Dimension::Lp(size) => self.allocated_space.1 += size,
|
||||
}
|
||||
min.into_px(scale).into_unsigned()
|
||||
min.into_upx(scale)
|
||||
}
|
||||
};
|
||||
self.layouts.insert(
|
||||
|
|
@ -397,8 +397,7 @@ impl Layout {
|
|||
) -> Size<UPx> {
|
||||
let (space_constraint, other_constraint) = self.orientation.split_size(available);
|
||||
let available_space = space_constraint.max();
|
||||
let allocated_space =
|
||||
self.allocated_space.0 + self.allocated_space.1.into_px(scale).into_unsigned();
|
||||
let allocated_space = self.allocated_space.0 + self.allocated_space.1.into_upx(scale);
|
||||
let mut remaining = available_space.saturating_sub(allocated_space);
|
||||
// If our `other_constraint` is not known, we will need to give child
|
||||
// widgets an opportunity to lay themselves out in the full area. This
|
||||
|
|
@ -450,9 +449,7 @@ impl Layout {
|
|||
let (_, measured) = self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation.make_size(
|
||||
ConstraintLimit::Known(
|
||||
self.layouts[index].size.into_px(scale).into_unsigned(),
|
||||
),
|
||||
ConstraintLimit::Known(self.layouts[index].size.into_upx(scale)),
|
||||
other_constraint,
|
||||
),
|
||||
!needs_final_layout,
|
||||
|
|
@ -475,9 +472,7 @@ impl Layout {
|
|||
self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation.make_size(
|
||||
ConstraintLimit::Known(
|
||||
self.layouts[index].size.into_px(scale).into_unsigned(),
|
||||
),
|
||||
ConstraintLimit::Known(self.layouts[index].size.into_upx(scale)),
|
||||
ConstraintLimit::Known(self.other),
|
||||
),
|
||||
true,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//! Types for displaying a [`Widget`](crate::widget::Widget) inside of a desktop
|
||||
//! window.
|
||||
//! Types for displaying a [`Widget`] inside of a desktop window.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::OsStr;
|
||||
|
|
@ -382,14 +381,14 @@ where
|
|||
.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());
|
||||
.then_some(Size::new(min_width, min_height).into_unsigned());
|
||||
|
||||
if new_min_size != self.min_inner_size && resizable {
|
||||
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());
|
||||
.then_some(Size::new(max_width, max_height).into_unsigned());
|
||||
|
||||
if new_max_size != self.max_inner_size && resizable {
|
||||
window.set_max_inner_size(new_max_size);
|
||||
|
|
|
|||
Loading…
Reference in a new issue