Radio, Checkbox refactor

This commit is contained in:
Jonathan Johnson 2023-11-22 10:48:26 -08:00
parent 23ba9ce11f
commit c9566fe1bd
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
8 changed files with 279 additions and 86 deletions

24
examples/radio.rs Normal file
View 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()
}

View file

@ -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();
}
}
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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
View 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)
}
}