mirror of
https://github.com/danbulant/cushy
synced 2026-06-17 21:41:11 +00:00
Default + Cancel widgets
This commit is contained in:
parent
5e055376e7
commit
bf9836a82b
11 changed files with 450 additions and 60 deletions
70
examples/login.rs
Normal file
70
examples/login.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use std::process::exit;
|
||||
|
||||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::{Align, Button, Expand, Input, Label, Resize, Stack};
|
||||
use gooey::{children, Run, WithClone};
|
||||
use kludgine::figures::units::Lp;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let username = Dynamic::default();
|
||||
let password = Dynamic::default();
|
||||
|
||||
// TODO This is absolutely horrible. The problem is that within for_each,
|
||||
// the value is still locked. Thus, we can't have a generic callback that
|
||||
// tries to lock the value that is being mapped in for_each.
|
||||
//
|
||||
// We might be able to make a genericized implementation for_each for
|
||||
// tuples, ie, (&Dynamic, &Dynamic).for_each(|(a, b)| ..).
|
||||
let valid = Dynamic::default();
|
||||
username.for_each((&valid, &password).with_clone(|(valid, password)| {
|
||||
move |username: &String| {
|
||||
password.map_ref(|password| valid.update(validate(username, password)))
|
||||
}
|
||||
}));
|
||||
password.for_each((&valid, &username).with_clone(|(valid, username)| {
|
||||
move |password: &String| {
|
||||
username.map_ref(|username| valid.update(validate(username, password)))
|
||||
}
|
||||
}));
|
||||
|
||||
Expand::new(Align::centered(Resize::width(
|
||||
// TODO We need a min/max range for the Resize widget
|
||||
Lp::points(400),
|
||||
Stack::rows(children![
|
||||
Stack::columns(children![
|
||||
Label::new("Username"),
|
||||
Expand::new(Align::centered(Input::new(username.clone())).fit_horizontally()),
|
||||
]),
|
||||
Stack::columns(children![
|
||||
Label::new("Password"),
|
||||
Expand::new(
|
||||
Align::centered(
|
||||
// TODO secure input
|
||||
Input::new(password.clone())
|
||||
)
|
||||
.fit_horizontally()
|
||||
),
|
||||
]),
|
||||
Stack::columns(children![
|
||||
Button::new("Cancel").on_click(|_| exit(0)).into_escape(),
|
||||
Expand::empty(),
|
||||
Button::new("Log In")
|
||||
.on_click(move |_| {
|
||||
if valid.get() {
|
||||
println!("Welcome, {}", username.get());
|
||||
exit(0);
|
||||
} else {
|
||||
eprintln!("Enter a username and password")
|
||||
}
|
||||
})
|
||||
.into_default(), // TODO enable/disable based on valid
|
||||
]),
|
||||
]),
|
||||
)))
|
||||
.run()
|
||||
}
|
||||
|
||||
fn validate(username: &String, password: &String) -> bool {
|
||||
!username.is_empty() && !password.is_empty()
|
||||
}
|
||||
|
|
@ -805,6 +805,28 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
self.pending_state.focus.as_ref() == Some(&self.current_node)
|
||||
}
|
||||
|
||||
/// Returns true if this widget is the target to activate when the user
|
||||
/// triggers a default action.
|
||||
///
|
||||
/// See
|
||||
/// [`MakeWidget::into_default()`](crate::widget::MakeWidget::into_default)
|
||||
/// for more information.
|
||||
#[must_use]
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.current_node.tree.default_widget() == Some(self.current_node.id())
|
||||
}
|
||||
|
||||
/// Returns true if this widget is the target to activate when the user
|
||||
/// triggers an escape action.
|
||||
///
|
||||
/// See
|
||||
/// [`MakeWidget::into_escape()`](crate::widget::MakeWidget::into_escape)
|
||||
/// for more information.
|
||||
#[must_use]
|
||||
pub fn is_escape(&self) -> bool {
|
||||
self.current_node.tree.escape_widget() == Some(self.current_node.id())
|
||||
}
|
||||
|
||||
/// Returns the widget this context is for.
|
||||
#[must_use]
|
||||
pub const fn widget(&self) -> &ManagedWidget {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,24 @@ impl ComponentDefinition for TextColor {
|
|||
}
|
||||
}
|
||||
|
||||
/// A [`Color`] to be used as a highlight color.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
pub struct PrimaryColor;
|
||||
|
||||
impl NamedComponent for PrimaryColor {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::named::<Global>("primary_color"))
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentDefinition for PrimaryColor {
|
||||
type ComponentType = Color;
|
||||
|
||||
fn default_value(&self) -> Color {
|
||||
Color::BLUE
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Color`] to be used as a highlight color.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
pub struct HighlightColor;
|
||||
|
|
|
|||
33
src/tree.rs
33
src/tree.rs
|
|
@ -32,6 +32,12 @@ impl Tree {
|
|||
styles: None,
|
||||
},
|
||||
);
|
||||
if widget.is_default() {
|
||||
data.defaults.push(id);
|
||||
}
|
||||
if widget.is_escape() {
|
||||
data.escapes.push(id);
|
||||
}
|
||||
if let Some(parent) = parent {
|
||||
let parent = data.nodes.get_mut(&parent.id()).expect("missing parent");
|
||||
parent.children.push(id);
|
||||
|
|
@ -48,6 +54,13 @@ impl Tree {
|
|||
pub fn remove_child(&self, child: &ManagedWidget, parent: &ManagedWidget) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
data.remove_child(child.id(), parent.id());
|
||||
|
||||
if child.widget.is_default() {
|
||||
data.defaults.retain(|id| *id != child.id());
|
||||
}
|
||||
if child.widget.is_escape() {
|
||||
data.escapes.retain(|id| *id != child.id());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_layout(&self, widget: WidgetId, rect: Rect<Px>) {
|
||||
|
|
@ -204,6 +217,24 @@ impl Tree {
|
|||
.hover
|
||||
}
|
||||
|
||||
pub fn default_widget(&self) -> Option<WidgetId> {
|
||||
self.data
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
.defaults
|
||||
.last()
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn escape_widget(&self) -> Option<WidgetId> {
|
||||
self.data
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
.escapes
|
||||
.last()
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn is_hovered(&self, id: WidgetId) -> bool {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut search = data.hover;
|
||||
|
|
@ -284,6 +315,8 @@ struct TreeData {
|
|||
active: Option<WidgetId>,
|
||||
focus: Option<WidgetId>,
|
||||
hover: Option<WidgetId>,
|
||||
defaults: Vec<WidgetId>,
|
||||
escapes: Vec<WidgetId>,
|
||||
render_order: Vec<WidgetId>,
|
||||
previous_focuses: HashMap<WidgetId, WidgetId>,
|
||||
}
|
||||
|
|
|
|||
130
src/widget.rs
130
src/widget.rs
|
|
@ -194,6 +194,34 @@ pub trait MakeWidget: Sized {
|
|||
fn with_next_focus(self, next_focus: impl IntoValue<Option<WidgetId>>) -> WidgetInstance {
|
||||
self.make_widget().with_next_focus(next_focus)
|
||||
}
|
||||
|
||||
/// Sets this widget as a "default" widget.
|
||||
///
|
||||
/// Default widgets are automatically activated when the user signals they
|
||||
/// are ready for the default action to occur.
|
||||
///
|
||||
/// Example widgets this is used for are:
|
||||
///
|
||||
/// - Submit buttons on forms
|
||||
/// - Ok buttons
|
||||
#[must_use]
|
||||
fn into_default(self) -> WidgetInstance {
|
||||
self.make_widget().into_default()
|
||||
}
|
||||
|
||||
/// Sets this widget as an "escape" widget.
|
||||
///
|
||||
/// Escape widgets are automatically activated when the user signals they
|
||||
/// are ready to escape their current situation.
|
||||
///
|
||||
/// Example widgets this is used for are:
|
||||
///
|
||||
/// - Close buttons
|
||||
/// - Cancel buttons
|
||||
#[must_use]
|
||||
fn into_escape(self) -> WidgetInstance {
|
||||
self.make_widget().into_escape()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that can create a [`WidgetInstance`] with a preallocated
|
||||
|
|
@ -265,9 +293,16 @@ where
|
|||
/// An instance of a [`Widget`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WidgetInstance {
|
||||
data: Arc<WidgetInstanceData>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WidgetInstanceData {
|
||||
id: WidgetId,
|
||||
widget: Arc<Mutex<dyn AnyWidget>>,
|
||||
default: bool,
|
||||
cancel: bool,
|
||||
next_focus: Value<Option<WidgetId>>,
|
||||
widget: Box<Mutex<dyn AnyWidget>>,
|
||||
}
|
||||
|
||||
impl WidgetInstance {
|
||||
|
|
@ -278,9 +313,13 @@ impl WidgetInstance {
|
|||
W: Widget,
|
||||
{
|
||||
Self {
|
||||
id: id.into(),
|
||||
widget: Arc::new(Mutex::new(widget)),
|
||||
next_focus: Value::default(),
|
||||
data: Arc::new(WidgetInstanceData {
|
||||
id: id.into(),
|
||||
next_focus: Value::default(),
|
||||
default: false,
|
||||
cancel: false,
|
||||
widget: Box::new(Mutex::new(widget)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,19 +334,70 @@ impl WidgetInstance {
|
|||
/// Returns the unique id of this widget instance.
|
||||
#[must_use]
|
||||
pub fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
self.data.id
|
||||
}
|
||||
|
||||
/// Sets the widget that should be focused next.
|
||||
///
|
||||
/// Gooey automatically determines reverse tab order by using this same
|
||||
/// relationship.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function can only be called when one instance of the widget exists.
|
||||
/// If any clones exist, a panic will occur.
|
||||
#[must_use]
|
||||
pub fn with_next_focus(
|
||||
mut self,
|
||||
next_focus: impl IntoValue<Option<WidgetId>>,
|
||||
) -> WidgetInstance {
|
||||
self.next_focus = next_focus.into_value();
|
||||
let data = Arc::get_mut(&mut self.data)
|
||||
.expect("with_next_focus can only be called on newly created widget instances");
|
||||
data.next_focus = next_focus.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets this widget as a "default" widget.
|
||||
///
|
||||
/// Default widgets are automatically activated when the user signals they
|
||||
/// are ready for the default action to occur.
|
||||
///
|
||||
/// Example widgets this is used for are:
|
||||
///
|
||||
/// - Submit buttons on forms
|
||||
/// - Ok buttons
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function can only be called when one instance of the widget exists.
|
||||
/// If any clones exist, a panic will occur.
|
||||
#[must_use]
|
||||
pub fn into_default(mut self) -> WidgetInstance {
|
||||
let data = Arc::get_mut(&mut self.data)
|
||||
.expect("with_next_focus can only be called on newly created widget instances");
|
||||
data.default = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets this widget as an "escape" widget.
|
||||
///
|
||||
/// Escape widgets are automatically activated when the user signals they
|
||||
/// are ready to escape their current situation.
|
||||
///
|
||||
/// Example widgets this is used for are:
|
||||
///
|
||||
/// - Close buttons
|
||||
/// - Cancel buttons
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function can only be called when one instance of the widget exists.
|
||||
/// If any clones exist, a panic will occur.
|
||||
#[must_use]
|
||||
pub fn into_escape(mut self) -> WidgetInstance {
|
||||
let data = Arc::get_mut(&mut self.data)
|
||||
.expect("with_next_focus can only be called on newly created widget instances");
|
||||
data.cancel = true;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +406,8 @@ impl WidgetInstance {
|
|||
/// occur due to other widget locks being held.
|
||||
pub fn lock(&self) -> WidgetGuard<'_> {
|
||||
WidgetGuard(
|
||||
self.widget
|
||||
self.data
|
||||
.widget
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g),
|
||||
)
|
||||
|
|
@ -333,12 +424,29 @@ impl WidgetInstance {
|
|||
/// This value comes from [`MakeWidget::with_next_focus()`].
|
||||
#[must_use]
|
||||
pub fn next_focus(&self) -> Option<WidgetId> {
|
||||
self.next_focus.get()
|
||||
self.data.next_focus.get()
|
||||
}
|
||||
|
||||
/// Returns true if this is a default widget.
|
||||
///
|
||||
/// See [`MakeWidget::into_default()`] for more information.
|
||||
#[must_use]
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.data.default
|
||||
}
|
||||
|
||||
/// Returns true if this is an escape widget.
|
||||
///
|
||||
/// See [`MakeWidget::into_escape()`] for more information.
|
||||
#[must_use]
|
||||
pub fn is_escape(&self) -> bool {
|
||||
self.data.cancel
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<WidgetId> for WidgetInstance {
|
||||
fn as_ref(&self) -> &WidgetId {
|
||||
&self.id
|
||||
&self.data.id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -346,7 +454,7 @@ impl Eq for WidgetInstance {}
|
|||
|
||||
impl PartialEq for WidgetInstance {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.widget, &other.widget)
|
||||
Arc::ptr_eq(&self.data, &other.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -435,7 +543,7 @@ impl ManagedWidget {
|
|||
/// Returns the unique id of this widget instance.
|
||||
#[must_use]
|
||||
pub fn id(&self) -> WidgetId {
|
||||
self.widget.id
|
||||
self.widget.id()
|
||||
}
|
||||
|
||||
/// Returns the next widget to focus after this widget.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use kludgine::figures::units::UPx;
|
|||
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
|
||||
use crate::context::{AsEventContext, GraphicsContext, LayoutContext};
|
||||
use crate::styles::{Edges, FlexibleDimension};
|
||||
use crate::styles::{Dimension, Edges, FlexibleDimension};
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, Widget, WidgetRef};
|
||||
use crate::ConstraintLimit;
|
||||
|
|
@ -32,6 +32,26 @@ impl Align {
|
|||
Self::new(FlexibleDimension::Auto, widget)
|
||||
}
|
||||
|
||||
/// Sets the left and right edges to 0 and returns self.
|
||||
#[must_use]
|
||||
pub fn fit_horizontally(mut self) -> Self {
|
||||
self.edges.map_mut(|edges| {
|
||||
edges.left = FlexibleDimension::Dimension(Dimension::default());
|
||||
edges.right = FlexibleDimension::Dimension(Dimension::default());
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the top and bottom edges to 0 and returns self.
|
||||
#[must_use]
|
||||
pub fn fit_vertically(mut self) -> Self {
|
||||
self.edges.map_mut(|edges| {
|
||||
edges.top = FlexibleDimension::Dimension(Dimension::default());
|
||||
edges.bottom = FlexibleDimension::Dimension(Dimension::default());
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
fn measure(
|
||||
&mut self,
|
||||
available_space: Size<ConstraintLimit>,
|
||||
|
|
@ -102,7 +122,7 @@ impl FrameInfo {
|
|||
fn measure(&self, available: ConstraintLimit, content: UPx) -> (UPx, UPx, UPx) {
|
||||
match available {
|
||||
ConstraintLimit::Known(size) => {
|
||||
let remaining = size - content;
|
||||
let remaining = size.saturating_sub(content);
|
||||
let (a, b) = match (self.a, self.b) {
|
||||
(Some(a), Some(b)) => (a, b),
|
||||
(Some(a), None) => (a, remaining - a),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ use std::panic::UnwindSafe;
|
|||
use std::time::Duration;
|
||||
|
||||
use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton};
|
||||
use kludgine::app::winit::keyboard::KeyCode;
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::shapes::Shape;
|
||||
|
|
@ -14,8 +13,11 @@ use kludgine::Color;
|
|||
use crate::animation::{AnimationHandle, AnimationTarget, Spawn};
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::names::Name;
|
||||
use crate::styles::components::{Easing, HighlightColor, IntrinsicPadding, TextColor};
|
||||
use crate::styles::components::{
|
||||
Easing, HighlightColor, IntrinsicPadding, PrimaryColor, TextColor,
|
||||
};
|
||||
use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent};
|
||||
use crate::utils::ModifiersExt;
|
||||
use crate::value::{Dynamic, IntoValue, Value};
|
||||
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
|
||||
|
||||
|
|
@ -66,12 +68,15 @@ impl Button {
|
|||
&ButtonActiveBackground,
|
||||
&ButtonBackground,
|
||||
&ButtonHoverBackground,
|
||||
&PrimaryColor,
|
||||
&Easing,
|
||||
]);
|
||||
let background_color = if context.active() {
|
||||
styles.get_or_default(&ButtonActiveBackground)
|
||||
} else if context.hovered() {
|
||||
styles.get_or_default(&ButtonHoverBackground)
|
||||
} else if context.is_default() {
|
||||
styles.get_or_default(&PrimaryColor)
|
||||
} else {
|
||||
styles.get_or_default(&ButtonBackground)
|
||||
};
|
||||
|
|
@ -237,13 +242,18 @@ impl Widget for Button {
|
|||
_is_synthetic: bool,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
if input.physical_key == KeyCode::Space {
|
||||
// TODO should this be handled at the window level?
|
||||
if input.text.as_deref() == Some(" ") && !context.modifiers().possible_shortcut() {
|
||||
let changed = match input.state {
|
||||
ElementState::Pressed => context.activate(),
|
||||
ElementState::Released => {
|
||||
self.invoke_on_click();
|
||||
context.deactivate()
|
||||
ElementState::Pressed => {
|
||||
let changed = context.activate();
|
||||
if !changed {
|
||||
// The widget was already active. This is now a repeated keypress
|
||||
self.invoke_on_click();
|
||||
}
|
||||
changed
|
||||
}
|
||||
ElementState::Released => context.deactivate(),
|
||||
};
|
||||
if changed {
|
||||
context.set_needs_redraw();
|
||||
|
|
@ -271,6 +281,11 @@ impl Widget for Button {
|
|||
}
|
||||
|
||||
fn activate(&mut self, context: &mut EventContext<'_, '_>) {
|
||||
// If we have no buttons pressed, the event should fire on activate not
|
||||
// on deactivate.
|
||||
if self.buttons_pressed == 0 {
|
||||
self.invoke_on_click();
|
||||
}
|
||||
self.update_background_color(context, true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,13 @@ pub struct Expand {
|
|||
/// The weight to use when splitting available space with multiple
|
||||
/// [`Expand`] widgets.
|
||||
pub weight: u8,
|
||||
child: WidgetRef,
|
||||
child: Option<WidgetRef>,
|
||||
}
|
||||
|
||||
impl Default for Expand {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Expand {
|
||||
|
|
@ -22,7 +28,16 @@ impl Expand {
|
|||
#[must_use]
|
||||
pub fn new(child: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
child: WidgetRef::new(child),
|
||||
child: Some(WidgetRef::new(child)),
|
||||
weight: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a widget that expands to fill its parent, but has no contents.
|
||||
#[must_use]
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
child: None,
|
||||
weight: 1,
|
||||
}
|
||||
}
|
||||
|
|
@ -34,21 +49,22 @@ impl Expand {
|
|||
#[must_use]
|
||||
pub fn weighted(weight: u8, child: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
child: WidgetRef::new(child),
|
||||
child: Some(WidgetRef::new(child)),
|
||||
weight,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the child widget.
|
||||
#[must_use]
|
||||
pub fn child(&self) -> &WidgetRef {
|
||||
&self.child
|
||||
pub fn child(&self) -> Option<&WidgetRef> {
|
||||
self.child.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Expand {
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let child = self.child.mounted(&mut context.as_event_context());
|
||||
let Some(child) = &mut self.child else { return };
|
||||
let child = child.mounted(&mut context.as_event_context());
|
||||
context.for_other(&child).redraw();
|
||||
}
|
||||
|
||||
|
|
@ -61,8 +77,15 @@ impl Widget for Expand {
|
|||
ConstraintLimit::Known(available_space.width.max()),
|
||||
ConstraintLimit::Known(available_space.height.max()),
|
||||
);
|
||||
let child = self.child.mounted(&mut context.as_event_context());
|
||||
let size = context.for_other(&child).layout(available_space);
|
||||
let child = self
|
||||
.child
|
||||
.as_mut()
|
||||
.map(|child| child.mounted(&mut context.as_event_context()));
|
||||
let size = if let Some(child) = &child {
|
||||
context.for_other(child).layout(available_space)
|
||||
} else {
|
||||
Size::default()
|
||||
};
|
||||
|
||||
let expanded_size = Size::new(
|
||||
available_space
|
||||
|
|
@ -72,7 +95,11 @@ impl Widget for Expand {
|
|||
.height
|
||||
.fit_measured(size.height, context.graphics.scale()),
|
||||
);
|
||||
context.set_child_layout(&child, Rect::from(expanded_size.into_signed()));
|
||||
|
||||
if let Some(child) = child {
|
||||
context.set_child_layout(&child, Rect::from(expanded_size.into_signed()));
|
||||
}
|
||||
|
||||
expanded_size
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,12 @@ impl Input {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Input {
|
||||
fn default() -> Self {
|
||||
Self::new(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Input {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Input")
|
||||
|
|
@ -393,8 +399,14 @@ impl Widget for Input {
|
|||
);
|
||||
(false, HANDLED)
|
||||
}
|
||||
(_, Some(text)) if !context.modifiers().state().primary() && text != "\t" => {
|
||||
editor.insert_string(&text, None);
|
||||
(_, Some(text))
|
||||
if !context.modifiers().primary()
|
||||
&& text != "\t" // tab
|
||||
&& text != "\r" // enter/return
|
||||
&& text != "\u{1b}" // escape
|
||||
=>
|
||||
{
|
||||
editor.insert_string(dbg!(&text), None);
|
||||
(true, HANDLED)
|
||||
}
|
||||
(_, _) => (false, IGNORED),
|
||||
|
|
@ -438,10 +450,12 @@ impl Widget for Input {
|
|||
|
||||
fn focus(&mut self, context: &mut EventContext<'_, '_>) {
|
||||
context.set_ime_allowed(true);
|
||||
context.set_needs_redraw();
|
||||
}
|
||||
|
||||
fn blur(&mut self, context: &mut EventContext<'_, '_>) {
|
||||
context.set_ime_allowed(false);
|
||||
context.set_needs_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContex
|
|||
use crate::styles::Dimension;
|
||||
use crate::value::{Generation, IntoValue, Value};
|
||||
use crate::widget::{Children, ManagedWidget, Widget, WidgetRef};
|
||||
use crate::widgets::{Expand, Resize};
|
||||
use crate::widgets::{Expand, Label, Resize};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A widget that displays a collection of [`Widgets`] in a
|
||||
|
|
@ -87,12 +87,21 @@ impl Stack {
|
|||
let guard = widget.lock();
|
||||
let (mut widget, dimension) =
|
||||
if let Some(expand) = guard.downcast_ref::<Expand>() {
|
||||
(
|
||||
expand.child().clone(),
|
||||
StackDimension::Fractional {
|
||||
weight: expand.weight,
|
||||
},
|
||||
)
|
||||
if let Some(child) = expand.child() {
|
||||
(
|
||||
child.clone(),
|
||||
StackDimension::Fractional {
|
||||
weight: expand.weight,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
WidgetRef::new(Label::new("")), // TODO this should be an empty widget.
|
||||
StackDimension::Fractional {
|
||||
weight: expand.weight,
|
||||
},
|
||||
)
|
||||
}
|
||||
} else if let Some((child, size)) =
|
||||
guard.downcast_ref::<Resize>().and_then(|r| {
|
||||
match self.layout.orientation.orientation {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use kludgine::app::winit::dpi::PhysicalPosition;
|
|||
use kludgine::app::winit::event::{
|
||||
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
||||
};
|
||||
use kludgine::app::winit::keyboard::KeyCode;
|
||||
use kludgine::app::winit::keyboard::Key;
|
||||
use kludgine::app::WindowBehavior as _;
|
||||
use kludgine::figures::units::Px;
|
||||
use kludgine::figures::{IntoSigned, Point, Rect, Size};
|
||||
|
|
@ -30,7 +30,9 @@ use crate::styles::components::VisualOrder;
|
|||
use crate::tree::Tree;
|
||||
use crate::utils::ModifiersExt;
|
||||
use crate::value::{Dynamic, IntoDynamic};
|
||||
use crate::widget::{EventHandling, ManagedWidget, Widget, WidgetInstance, HANDLED, IGNORED};
|
||||
use crate::widget::{
|
||||
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
|
||||
};
|
||||
use crate::window::sealed::WindowCommand;
|
||||
use crate::{ConstraintLimit, Run};
|
||||
|
||||
|
|
@ -243,6 +245,7 @@ struct GooeyWindow<T> {
|
|||
initial_frame: bool,
|
||||
occluded: Dynamic<bool>,
|
||||
focused: Dynamic<bool>,
|
||||
keyboard_activated: Option<ManagedWidget>,
|
||||
}
|
||||
|
||||
impl<T> GooeyWindow<T>
|
||||
|
|
@ -254,6 +257,38 @@ where
|
|||
|
||||
self.should_close
|
||||
}
|
||||
|
||||
fn keyboard_activate_widget(
|
||||
&mut self,
|
||||
is_pressed: bool,
|
||||
widget: Option<WidgetId>,
|
||||
window: &mut RunningWindow<'_>,
|
||||
kludgine: &mut Kludgine,
|
||||
) {
|
||||
if is_pressed {
|
||||
if let Some(default) = widget.and_then(|id| self.root.tree.widget(id)) {
|
||||
if let Some(previously_active) = self.keyboard_activated.take() {
|
||||
EventContext::new(
|
||||
WidgetContext::new(previously_active, &self.redraw_status, window),
|
||||
kludgine,
|
||||
)
|
||||
.deactivate();
|
||||
}
|
||||
EventContext::new(
|
||||
WidgetContext::new(default.clone(), &self.redraw_status, window),
|
||||
kludgine,
|
||||
)
|
||||
.activate();
|
||||
self.keyboard_activated = Some(default);
|
||||
}
|
||||
} else if let Some(keyboard_activated) = self.keyboard_activated.take() {
|
||||
EventContext::new(
|
||||
WidgetContext::new(keyboard_activated, &self.redraw_status, window),
|
||||
kludgine,
|
||||
)
|
||||
.deactivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> kludgine::app::WindowBehavior<WindowCommand> for GooeyWindow<T>
|
||||
|
|
@ -299,6 +334,7 @@ where
|
|||
initial_frame: true,
|
||||
occluded,
|
||||
focused,
|
||||
keyboard_activated: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -442,32 +478,50 @@ where
|
|||
drop(target);
|
||||
|
||||
if !handled {
|
||||
match input.physical_key {
|
||||
KeyCode::KeyW
|
||||
if window.modifiers().state().primary() && input.state.is_pressed() =>
|
||||
{
|
||||
if self.request_close(&mut window) {
|
||||
match input.logical_key {
|
||||
Key::Character(ch) if ch == "w" && window.modifiers().primary() => {
|
||||
if input.state.is_pressed() && self.request_close(&mut window) {
|
||||
window.set_needs_redraw();
|
||||
}
|
||||
}
|
||||
KeyCode::Tab
|
||||
if !window.modifiers().state().possible_shortcut()
|
||||
&& input.state.is_pressed() =>
|
||||
{
|
||||
let direction = if window.modifiers().state().shift_key() {
|
||||
VisualOrder::left_to_right().rev()
|
||||
} else {
|
||||
VisualOrder::left_to_right()
|
||||
};
|
||||
let target = self.root.tree.focused_widget().unwrap_or(self.root.id());
|
||||
let target = self.root.tree.widget(target).expect("missing widget");
|
||||
let mut target = EventContext::new(
|
||||
WidgetContext::new(target, &self.redraw_status, &mut window),
|
||||
Key::Tab if !window.modifiers().possible_shortcut() => {
|
||||
if input.state.is_pressed() {
|
||||
let direction = if window.modifiers().state().shift_key() {
|
||||
VisualOrder::left_to_right().rev()
|
||||
} else {
|
||||
VisualOrder::left_to_right()
|
||||
};
|
||||
let target = self.root.tree.focused_widget().unwrap_or(self.root.id());
|
||||
let target = self.root.tree.widget(target).expect("missing widget");
|
||||
let mut target = EventContext::new(
|
||||
WidgetContext::new(target, &self.redraw_status, &mut window),
|
||||
kludgine,
|
||||
);
|
||||
target.advance_focus(direction);
|
||||
}
|
||||
}
|
||||
Key::Enter => {
|
||||
self.keyboard_activate_widget(
|
||||
input.state.is_pressed(),
|
||||
self.root.tree.default_widget(),
|
||||
&mut window,
|
||||
kludgine,
|
||||
);
|
||||
target.advance_focus(direction);
|
||||
}
|
||||
_ => {}
|
||||
Key::Escape => {
|
||||
self.keyboard_activate_widget(
|
||||
input.state.is_pressed(),
|
||||
self.root.tree.escape_widget(),
|
||||
&mut window,
|
||||
kludgine,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"Ignored Keyboard Input: {:?} ({:?}); {:?}",
|
||||
input.logical_key, input.physical_key, input.state
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue