mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 06:21:15 +00:00
Radio, Checkbox refactor
This commit is contained in:
parent
23ba9ce11f
commit
c9566fe1bd
8 changed files with 279 additions and 86 deletions
24
examples/radio.rs
Normal file
24
examples/radio.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::Run;
|
||||
|
||||
#[derive(Default, Eq, PartialEq, Debug, Clone, Copy)]
|
||||
pub enum Choice {
|
||||
#[default]
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
}
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let option = Dynamic::default();
|
||||
|
||||
option
|
||||
.new_radio(Choice::A, "A")
|
||||
.and(option.new_radio(Choice::B, "B"))
|
||||
.and(option.new_radio(Choice::C, "C"))
|
||||
.into_rows()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
102
src/context.rs
102
src/context.rs
|
|
@ -1,6 +1,5 @@
|
|||
//! Types that provide access to the Gooey runtime.
|
||||
use std::borrow::Cow;
|
||||
use std::hash::Hash;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
|
@ -14,6 +13,7 @@ use kludgine::figures::{IntoSigned, Point, Px2D, Rect, Round, ScreenScale, Size,
|
|||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::{Color, Kludgine};
|
||||
|
||||
use crate::context::sealed::WindowHandle;
|
||||
use crate::graphics::Graphics;
|
||||
use crate::styles::components::{
|
||||
CornerRadius, FontFamily, FontStyle, FontWeight, HighlightColor, LayoutOrder, TextSize,
|
||||
|
|
@ -21,9 +21,8 @@ use crate::styles::components::{
|
|||
};
|
||||
use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair};
|
||||
use crate::utils::IgnorePoison;
|
||||
use crate::value::{Dynamic, IntoValue, Value};
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef};
|
||||
use crate::window::sealed::WindowCommand;
|
||||
use crate::window::{CursorState, RunningWindow, ThemeMode};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
|
|
@ -940,12 +939,12 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
}
|
||||
|
||||
/// Ensures that this widget will be redrawn when `value` has been updated.
|
||||
pub fn redraw_when_changed<T>(&self, value: &Dynamic<T>) {
|
||||
pub fn redraw_when_changed(&self, value: &impl Trackable) {
|
||||
value.redraw_when_changed(self.handle());
|
||||
}
|
||||
|
||||
/// Ensures that this widget will be redrawn when `value` has been updated.
|
||||
pub fn invalidate_when_changed<T>(&self, value: &Dynamic<T>) {
|
||||
pub fn invalidate_when_changed(&self, value: &impl Trackable) {
|
||||
value.invalidate_when_changed(self.handle(), self.current_node.id());
|
||||
}
|
||||
|
||||
|
|
@ -1166,43 +1165,6 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct WindowHandle {
|
||||
kludgine: kludgine::app::WindowHandle<WindowCommand>,
|
||||
redraw_status: InvalidationStatus,
|
||||
}
|
||||
|
||||
impl Eq for WindowHandle {}
|
||||
|
||||
impl PartialEq for WindowHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(
|
||||
&self.redraw_status.invalidated,
|
||||
&other.redraw_status.invalidated,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for WindowHandle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
Arc::as_ptr(&self.redraw_status.invalidated).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowHandle {
|
||||
pub fn redraw(&self) {
|
||||
if self.redraw_status.should_send_refresh() {
|
||||
let _result = self.kludgine.send(WindowCommand::Redraw);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate(&self, widget: WidgetId) {
|
||||
if self.redraw_status.invalidate(widget) {
|
||||
self.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn AsEventContext<'_> {}
|
||||
|
||||
impl Drop for EventContext<'_, '_> {
|
||||
|
|
@ -1372,3 +1334,59 @@ impl Default for WidgetCacheKey {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that can be tracked to refresh or invalidate widgets.
|
||||
pub trait Trackable: sealed::Trackable {}
|
||||
|
||||
impl<T> Trackable for T where T: sealed::Trackable {}
|
||||
|
||||
pub(crate) mod sealed {
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::context::InvalidationStatus;
|
||||
use crate::widget::WidgetId;
|
||||
use crate::window::sealed::WindowCommand;
|
||||
|
||||
pub trait Trackable {
|
||||
fn redraw_when_changed(&self, handle: WindowHandle);
|
||||
fn invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WindowHandle {
|
||||
pub(crate) kludgine: kludgine::app::WindowHandle<WindowCommand>,
|
||||
pub(crate) redraw_status: InvalidationStatus,
|
||||
}
|
||||
|
||||
impl Eq for WindowHandle {}
|
||||
|
||||
impl PartialEq for WindowHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(
|
||||
&self.redraw_status.invalidated,
|
||||
&other.redraw_status.invalidated,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for WindowHandle {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
Arc::as_ptr(&self.redraw_status.invalidated).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowHandle {
|
||||
pub fn redraw(&self) {
|
||||
if self.redraw_status.should_send_refresh() {
|
||||
let _result = self.kludgine.send(WindowCommand::Redraw);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate(&self, widget: WidgetId) {
|
||||
if self.redraw_status.invalidate(widget) {
|
||||
self.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use alot::{LotId, Lots};
|
|||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{Point, Rect, Size};
|
||||
|
||||
use crate::context::WindowHandle;
|
||||
use crate::context::sealed::WindowHandle;
|
||||
use crate::styles::{Styles, ThemePair, VisualOrder};
|
||||
use crate::utils::IgnorePoison;
|
||||
use crate::value::Value;
|
||||
|
|
|
|||
42
src/value.rs
42
src/value.rs
|
|
@ -13,10 +13,11 @@ use ahash::AHashSet;
|
|||
use intentional::Assert;
|
||||
|
||||
use crate::animation::{DynamicTransition, LinearInterpolate};
|
||||
use crate::context::{WidgetContext, WindowHandle};
|
||||
use crate::context::sealed::WindowHandle;
|
||||
use crate::context::{self, WidgetContext};
|
||||
use crate::utils::{IgnorePoison, UnwindsafeCondvar, WithClone};
|
||||
use crate::widget::{WidgetId, WidgetInstance};
|
||||
use crate::widgets::Switcher;
|
||||
use crate::widget::{MakeWidget, WidgetId, WidgetInstance};
|
||||
use crate::widgets::{Radio, Switcher};
|
||||
|
||||
/// An instance of a value that provides APIs to observe and react to its
|
||||
/// contents.
|
||||
|
|
@ -474,6 +475,21 @@ impl<T> Dynamic<T> {
|
|||
new_value,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new [`Radio`] that updates this dynamic to `widget_value` when
|
||||
/// pressed. `label` is drawn next to the checkbox and is also clickable to
|
||||
/// select the radio.
|
||||
#[must_use]
|
||||
pub fn new_radio(&self, widget_value: T, label: impl MakeWidget) -> Radio<T>
|
||||
where
|
||||
Self: Clone,
|
||||
// Technically this trait bound isn't necessary, but it prevents trying
|
||||
// to call into_radio on unsupported types. The MakeWidget/Widget
|
||||
// implementations require these bounds (and more).
|
||||
T: Clone + Eq,
|
||||
{
|
||||
Radio::new(widget_value, self.clone(), label)
|
||||
}
|
||||
}
|
||||
|
||||
impl Dynamic<WidgetInstance> {
|
||||
|
|
@ -485,6 +501,16 @@ impl Dynamic<WidgetInstance> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> context::sealed::Trackable for Dynamic<T> {
|
||||
fn redraw_when_changed(&self, handle: WindowHandle) {
|
||||
self.redraw_when_changed(handle);
|
||||
}
|
||||
|
||||
fn invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) {
|
||||
self.invalidate_when_changed(handle, id);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Dynamic<T>
|
||||
where
|
||||
T: Default,
|
||||
|
|
@ -919,6 +945,16 @@ impl<T> DynamicReader<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> context::sealed::Trackable for DynamicReader<T> {
|
||||
fn redraw_when_changed(&self, handle: WindowHandle) {
|
||||
self.source.redraw_when_changed(handle);
|
||||
}
|
||||
|
||||
fn invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) {
|
||||
self.source.invalidate_when_changed(handle, id);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for DynamicReader<T> {
|
||||
fn clone(&self) -> Self {
|
||||
self.source.state().expect("deadlocked").readers += 1;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@ use kludgine::figures::units::{Px, UPx};
|
|||
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::context::{
|
||||
AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext, WindowHandle,
|
||||
};
|
||||
use crate::context::sealed::WindowHandle;
|
||||
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::styles::{
|
||||
ComponentDefinition, ContainerLevel, Dimension, DimensionRange, Edges, IntoComponentValue,
|
||||
Styles, ThemePair, VisualOrder,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub mod input;
|
|||
pub mod label;
|
||||
mod mode_switch;
|
||||
pub mod progress;
|
||||
mod radio;
|
||||
mod resize;
|
||||
pub mod scroll;
|
||||
pub mod slider;
|
||||
|
|
@ -34,6 +35,7 @@ pub use input::Input;
|
|||
pub use label::Label;
|
||||
pub use mode_switch::ThemedMode;
|
||||
pub use progress::ProgressBar;
|
||||
pub use radio::Radio;
|
||||
pub use resize::Resize;
|
||||
pub use scroll::Scroll;
|
||||
pub use slider::Slider;
|
||||
|
|
|
|||
|
|
@ -3,16 +3,14 @@ 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::figures::units::Lp;
|
||||
use kludgine::figures::{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::styles::components::{LineHeight, OutlineColor, TextColor, WidgetAccentColor};
|
||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, WidgetInstance, WidgetRef, WrappedLayout, WrapperWidget};
|
||||
use crate::widget::{MakeWidget, Widget, WidgetInstance};
|
||||
use crate::widgets::button::ButtonKind;
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
|
|
@ -56,8 +54,9 @@ impl MakeWidget for Checkbox {
|
|||
fn make_widget(self) -> WidgetInstance {
|
||||
CheckboxLabel {
|
||||
value: self.state.create_reader(),
|
||||
label: WidgetRef::new(self.label),
|
||||
}
|
||||
.and(self.label)
|
||||
.into_columns()
|
||||
.into_button()
|
||||
.on_click(move |()| {
|
||||
let mut value = self.state.lock();
|
||||
|
|
@ -172,39 +171,18 @@ 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
|
||||
}
|
||||
impl Widget for CheckboxLabel {
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let checkbox_size = context
|
||||
.gfx
|
||||
.region()
|
||||
.size
|
||||
.width
|
||||
.min(context.gfx.region().size.height);
|
||||
|
||||
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 * 2;
|
||||
let effective_height = size.height.max(label_inset);
|
||||
let size_with_checkbox =
|
||||
Size::new(size.width + label_inset + padding, effective_height).into_unsigned();
|
||||
WrappedLayout {
|
||||
child: Rect::new(
|
||||
Point::new(label_inset, Px::ZERO),
|
||||
Size::new(size.width, effective_height),
|
||||
),
|
||||
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::squared(padding), Size::squared(checkbox_size));
|
||||
let checkbox_rect = Rect::from(Size::squared(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) => {
|
||||
|
|
@ -250,6 +228,15 @@ impl WrapperWidget for CheckboxLabel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<kludgine::figures::units::UPx> {
|
||||
let checkbox_size = context.get(&LineHeight).into_upx(context.gfx.scale()); // TODO create a component?
|
||||
Size::squared(checkbox_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that can be used as a checkbox.
|
||||
|
|
|
|||
127
src/widgets/radio.rs
Normal file
127
src/widgets/radio.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
//! A tri-state, labelable checkbox widget.
|
||||
use std::fmt::Debug;
|
||||
use std::panic::UnwindSafe;
|
||||
|
||||
use kludgine::figures::units::Lp;
|
||||
use kludgine::figures::{Point, ScreenScale, Size};
|
||||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::DrawableExt;
|
||||
|
||||
use crate::context::{GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::{LineHeight, OutlineColor, WidgetAccentColor};
|
||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, Widget, WidgetInstance};
|
||||
use crate::widgets::button::ButtonKind;
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A labeled-widget that supports three states: Checked, Unchecked, and
|
||||
/// Indeterminant
|
||||
pub struct Radio<T> {
|
||||
/// The value this button represents.
|
||||
pub value: T,
|
||||
/// The state (value) of the checkbox.
|
||||
pub state: Dynamic<T>,
|
||||
/// The button kind to use as the basis for this radio. Radios default to
|
||||
/// [`ButtonKind::Transparent`].
|
||||
pub kind: Value<ButtonKind>,
|
||||
label: WidgetInstance,
|
||||
}
|
||||
|
||||
impl<T> Radio<T> {
|
||||
/// Returns a new radio that sets `state` to `value` when pressed. `label`
|
||||
/// is drawn next to the checkbox and is also clickable to select the radio.
|
||||
pub fn new(value: T, state: impl IntoDynamic<T>, label: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
value,
|
||||
state: state.into_dynamic(),
|
||||
kind: Value::Constant(ButtonKind::Transparent),
|
||||
label: label.make_widget(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the button kind to use as the basis for this radio, and
|
||||
/// returns self.
|
||||
///
|
||||
/// Radios default to [`ButtonKind::Transparent`].
|
||||
#[must_use]
|
||||
pub fn kind(mut self, kind: impl IntoValue<ButtonKind>) -> Self {
|
||||
self.kind = kind.into_value();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MakeWidget for Radio<T>
|
||||
where
|
||||
T: Clone + Debug + Eq + UnwindSafe + Send + 'static,
|
||||
{
|
||||
fn make_widget(self) -> WidgetInstance {
|
||||
RadioOrnament {
|
||||
value: self.value.clone(),
|
||||
state: self.state.create_reader(),
|
||||
}
|
||||
.and(self.label)
|
||||
.into_columns()
|
||||
.into_button()
|
||||
.on_click(move |()| {
|
||||
self.state.update(self.value.clone());
|
||||
})
|
||||
.kind(self.kind)
|
||||
.make_widget()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RadioOrnament<T> {
|
||||
value: T,
|
||||
state: DynamicReader<T>,
|
||||
}
|
||||
|
||||
impl<T> Widget for RadioOrnament<T>
|
||||
where
|
||||
T: Debug + Eq + UnwindSafe + Send + 'static,
|
||||
{
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let radio_size = context
|
||||
.gfx
|
||||
.region()
|
||||
.size
|
||||
.width
|
||||
.min(context.gfx.region().size.height);
|
||||
let vertical_center = context.gfx.region().size.height / 2;
|
||||
|
||||
let stroke_options = StrokeOptions::lp_wide(Lp::points(2)).into_px(context.gfx.scale());
|
||||
context.redraw_when_changed(&self.state);
|
||||
let selected = self.state.map_ref(|state| state == &self.value);
|
||||
let color = context.get(&OutlineColor);
|
||||
let radius = radio_size / 2;
|
||||
context.gfx.draw_shape(
|
||||
Shape::stroked_circle(
|
||||
radius - stroke_options.line_width / 2,
|
||||
color,
|
||||
kludgine::Origin::Center,
|
||||
stroke_options.colored(color),
|
||||
)
|
||||
.translate_by(Point::new(radius, vertical_center)),
|
||||
);
|
||||
if selected {
|
||||
let color = context.get(&WidgetAccentColor);
|
||||
context.gfx.draw_shape(
|
||||
Shape::filled_circle(
|
||||
radius - stroke_options.line_width * 2,
|
||||
color,
|
||||
kludgine::Origin::Center,
|
||||
)
|
||||
.translate_by(Point::new(radius, vertical_center)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<kludgine::figures::units::UPx> {
|
||||
let radio_size = context.get(&LineHeight).into_upx(context.gfx.scale()); // TODO create a component? Same as checkbox
|
||||
Size::squared(radio_size)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue