Skip to content

Commit 6de93b2

Browse files
xarvicxarviccmyr
authored
Disable widgets (#1632)
* - added LifeCycle::DisabledChanged and InternalLifeCycle::RouteDisabledChanged - implemented the disabled state in WidgetPod - changed call to focus_change from event to post event processing - implemented disabled handling in window.rs and core.rs * created tests for disable * fixed tests * fixed tests * updated core.rs and event.rs * fixed focus-chain bug: - the focus chain was cleared, if the widget was disabled * fix disabled update * update tests * fixed code (all tests succeed) * refactored core.rs and tests/mod.rs * updated tests * fixed focus-chain bug * make clippy happy (i hope) * make clippy happy #2 * Apply suggestions from code review Update Documentation Co-authored-by: Colin Rofls <colin@cmyr.net> * Update druid/src/contexts.rs Update documentation Co-authored-by: Colin Rofls <colin@cmyr.net> * refactor DisabledChanged * refactor DisabledChanged * fixed error, revered change of focus_chain * refactored tests * reordered lifecycle events * reverted changes to the focus_chain * implemented new focus-chain using LifeCycle::BuildFocusChain * update tests * fixed problems * updated texts * clippy fix * fixed documentation * Update druid/src/event.rs Co-authored-by: Colin Rofls <colin@cmyr.net> * fixed documentation * made logic simpler * refactored post_event_processing * updated CHANGELOG.md * fixed docs * make clippy happy Co-authored-by: xarvic <xarvix@web.de> Co-authored-by: Colin Rofls <colin@cmyr.net>
1 parent 39238ad commit 6de93b2

File tree

8 files changed

+628
-46
lines changed

8 files changed

+628
-46
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ You can find its changes [documented below](#070---2021-01-01).
3030
- `Notification`s can be submitted while handling other `Notification`s ([#1640] by [@cmyr])
3131
- Added ListIter implementations for OrdMap ([#1641] by [@Lejero])
3232
- `Padding` can now use `Key<Insets>` ([#1662] by [@cmyr])
33+
- `LifeCycle::DisabledChanged`, `InternalLifeCycle::RouteDisabledChanged` and the `set_disabled()` and `is_disabled()`
34+
context-methods to implement disabled ([#1632] by [@xarvic])
35+
- `LifeCycle::BuildFocusChain` to update the focus-chain ([#1632] by [@xarvic])
3336

3437
### Changed
3538

@@ -40,6 +43,7 @@ You can find its changes [documented below](#070---2021-01-01).
4043
- Padding is generic over child widget, impls WidgetWrapper ([#1634] by [@cmyr])
4144
- Menu support was rewritten with support for `Data` ([#1625] by [@jneem])
4245
- Update to piet v0.4.0 (rich text on linux!) ([#1677] by [@cmyr])
46+
- `register_for_focus()` should from now on be called from `LifeCycle::BuildFocusChain` instead of `LifeCycle::WidgetAdded` ([#1632] by [@xarvic])
4347
- Flex values that are less than 0.0 will default to 0.0 and warn in release. It will panic in debug mode. ([#1691] by [@arthmis])
4448

4549
### Deprecated
@@ -444,6 +448,7 @@ Last release without a changelog :(
444448
[@SecondFlight]: https://github.com/SecondFlight
445449
[@lord]: https://github.com/lord
446450
[@Lejero]: https://github.com/Lejero
451+
[@xarvic]: https://github.com/xarvic
447452
[@arthmis]: https://github.com/arthmis
448453
[@ccqpein]: https://github.com/ccqpein
449454

@@ -653,6 +658,7 @@ Last release without a changelog :(
653658
[#1606]: https://github.com/linebender/druid/pull/1606
654659
[#1619]: https://github.com/linebender/druid/pull/1619
655660
[#1625]: https://github.com/linebender/druid/pull/1625
661+
[#1632]: https://github.com/linebender/druid/pull/1632
656662
[#1634]: https://github.com/linebender/druid/pull/1634
657663
[#1635]: https://github.com/linebender/druid/pull/1635
658664
[#1636]: https://github.com/linebender/druid/pull/1636

druid/src/contexts.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,23 @@ impl_context_method!(
276276
pub fn has_focus(&self) -> bool {
277277
self.widget_state.has_focus
278278
}
279+
280+
/// The disabled state of a widget.
281+
///
282+
/// Returns `true` if this widget or any of its ancestors is explicitly disabled.
283+
/// To make this widget explicitly disabled use [`set_disabled`].
284+
///
285+
/// Disabled means that this widget should not change the state of the application. What
286+
/// that means is not entirely clear but in any it should not change its data. Therefore
287+
/// others can use this as a safety mechanism to prevent the application from entering an
288+
/// illegal state.
289+
/// For an example the decrease button of a counter of type `usize` should be disabled if the
290+
/// value is `0`.
291+
///
292+
/// [`set_disabled`]: EventCtx::set_disabled
293+
pub fn is_disabled(&self) -> bool {
294+
self.widget_state.is_disabled()
295+
}
279296
}
280297
);
281298

@@ -374,9 +391,25 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>,
374391
pub fn children_changed(&mut self) {
375392
trace!("children_changed");
376393
self.widget_state.children_changed = true;
394+
self.widget_state.update_focus_chain = true;
377395
self.request_layout();
378396
}
379397

398+
/// Set the disabled state for this widget.
399+
///
400+
/// Setting this to `false` does not mean a widget is not still disabled; for instance it may
401+
/// still be disabled by an ancestor. See [`is_disabled`] for more information.
402+
///
403+
/// Calling this method during [`LifeCycle::DisabledChanged`] has no effect.
404+
///
405+
/// [`LifeCycle::DisabledChanged`]: struct.LifeCycle.html#variant.DisabledChanged
406+
/// [`is_disabled`]: EventCtx::is_disabled
407+
pub fn set_disabled(&mut self, disabled: bool) {
408+
// widget_state.children_disabled_changed is not set because we want to be able to delete
409+
// changes that happened during DisabledChanged.
410+
self.widget_state.is_explicitly_disabled_new = disabled;
411+
}
412+
380413
/// Indicate that text input state has changed.
381414
///
382415
/// A widget that accepts text input should call this anytime input state

druid/src/core.rs

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ pub(crate) struct WidgetState {
106106
pub(crate) viewport_offset: Vec2,
107107

108108
// TODO: consider using bitflags for the booleans.
109+
// `true` if a descendent of this widget changed its disabled state and should receive
110+
// LifeCycle::DisabledChanged or InternalLifeCycle::RouteDisabledChanged
111+
pub(crate) children_disabled_changed: bool,
112+
113+
// `true` if one of our ancestors is disabled (meaning we are also disabled).
114+
pub(crate) ancestor_disabled: bool,
115+
116+
// `true` if this widget has been explicitly disabled.
117+
// A widget can be disabled without being *explicitly* disabled if an ancestor is disabled.
118+
pub(crate) is_explicitly_disabled: bool,
119+
120+
// `true` if this widget has been explicitly disabled, but has not yet seen one of
121+
// LifeCycle::DisabledChanged or InternalLifeCycle::RouteDisabledChanged
122+
pub(crate) is_explicitly_disabled_new: bool,
123+
109124
pub(crate) is_hot: bool,
110125

111126
pub(crate) is_active: bool,
@@ -125,6 +140,8 @@ pub(crate) struct WidgetState {
125140
/// Any descendant has requested update.
126141
pub(crate) request_update: bool,
127142

143+
pub(crate) update_focus_chain: bool,
144+
128145
pub(crate) focus_chain: Vec<WidgetId>,
129146
pub(crate) request_focus: Option<FocusChange>,
130147
pub(crate) children: Bloom<WidgetId>,
@@ -908,11 +925,25 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
908925
} else {
909926
if self.state.children_changed {
910927
self.state.children.clear();
911-
self.state.focus_chain.clear();
912928
}
913929
self.state.children_changed
914930
}
915931
}
932+
InternalLifeCycle::RouteDisabledChanged => {
933+
self.state.update_focus_chain = true;
934+
935+
let was_disabled = self.state.is_disabled();
936+
937+
self.state.is_explicitly_disabled = self.state.is_explicitly_disabled_new;
938+
939+
if was_disabled != self.state.is_disabled() {
940+
extra_event = Some(LifeCycle::DisabledChanged(self.state.is_disabled()));
941+
//Each widget needs only one of DisabledChanged and RouteDisabledChanged
942+
false
943+
} else {
944+
self.state.children_disabled_changed
945+
}
946+
}
916947
InternalLifeCycle::RouteFocusChanged { old, new } => {
917948
let this_changed = if *old == Some(self.state.id) {
918949
Some(false)
@@ -962,6 +993,8 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
962993
assert!(self.old_data.is_none());
963994
trace!("Received LifeCycle::WidgetAdded");
964995

996+
self.state.update_focus_chain = true;
997+
965998
self.old_data = Some(data.clone());
966999
self.env = Some(env.clone());
9671000

@@ -980,13 +1013,34 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
9801013
// This event was meant only for our parent, so don't recurse.
9811014
false
9821015
}
1016+
LifeCycle::DisabledChanged(ancestors_disabled) => {
1017+
self.state.update_focus_chain = true;
1018+
1019+
let was_disabled = self.state.is_disabled();
1020+
1021+
self.state.is_explicitly_disabled = self.state.is_explicitly_disabled_new;
1022+
self.state.ancestor_disabled = *ancestors_disabled;
1023+
1024+
// the change direction (true -> false or false -> true) of our parent and ourself
1025+
// is always the same, or we dont change at all, because we stay disabled if either
1026+
// we or our parent are disabled.
1027+
was_disabled != self.state.is_disabled()
1028+
}
9831029
//NOTE: this is not sent here, but from the special set_hot_state method
9841030
LifeCycle::HotChanged(_) => false,
9851031
LifeCycle::FocusChanged(_) => {
9861032
// We are a descendant of a widget that has/had focus.
9871033
// Descendants don't inherit focus, so don't recurse.
9881034
false
9891035
}
1036+
LifeCycle::BuildFocusChain => {
1037+
if self.state.update_focus_chain {
1038+
self.state.focus_chain.clear();
1039+
true
1040+
} else {
1041+
false
1042+
}
1043+
}
9901044
};
9911045

9921046
let mut child_ctx = LifeCycleCtx {
@@ -1002,18 +1056,40 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
10021056
self.inner.lifecycle(&mut child_ctx, event, data, env);
10031057
}
10041058

1005-
ctx.widget_state.merge_up(&mut self.state);
1059+
// Sync our state with our parent's state after the event!
10061060

1007-
// we need to (re)register children in case of one of the following events
10081061
match event {
1062+
// we need to (re)register children in case of one of the following events
10091063
LifeCycle::WidgetAdded | LifeCycle::Internal(InternalLifeCycle::RouteWidgetAdded) => {
10101064
self.state.children_changed = false;
10111065
ctx.widget_state.children = ctx.widget_state.children.union(self.state.children);
1012-
ctx.widget_state.focus_chain.extend(&self.state.focus_chain);
10131066
ctx.register_child(self.id());
10141067
}
1068+
LifeCycle::DisabledChanged(_)
1069+
| LifeCycle::Internal(InternalLifeCycle::RouteDisabledChanged) => {
1070+
self.state.children_disabled_changed = false;
1071+
1072+
if self.state.is_disabled() && self.state.has_focus {
1073+
// This may gets overwritten. This is ok because it still ensures that a
1074+
// FocusChange is routed after we updated the focus-chain.
1075+
self.state.request_focus = Some(FocusChange::Resign);
1076+
}
1077+
1078+
// Delete changes of disabled state that happened during DisabledChanged to avoid
1079+
// recursions.
1080+
self.state.is_explicitly_disabled_new = self.state.is_explicitly_disabled;
1081+
}
1082+
// Update focus-chain of our parent
1083+
LifeCycle::BuildFocusChain => {
1084+
self.state.update_focus_chain = false;
1085+
if !self.state.is_disabled() {
1086+
ctx.widget_state.focus_chain.extend(&self.state.focus_chain);
1087+
}
1088+
}
10151089
_ => (),
10161090
}
1091+
1092+
ctx.widget_state.merge_up(&mut self.state);
10171093
}
10181094

10191095
/// Propagate a data update.
@@ -1114,6 +1190,9 @@ impl WidgetState {
11141190
paint_insets: Insets::ZERO,
11151191
invalid: Region::EMPTY,
11161192
viewport_offset: Vec2::ZERO,
1193+
children_disabled_changed: false,
1194+
ancestor_disabled: false,
1195+
is_explicitly_disabled: false,
11171196
baseline_offset: 0.0,
11181197
is_hot: false,
11191198
needs_layout: false,
@@ -1130,10 +1209,21 @@ impl WidgetState {
11301209
cursor_change: CursorChange::Default,
11311210
cursor: None,
11321211
sub_window_hosts: Vec::new(),
1212+
is_explicitly_disabled_new: false,
11331213
text_registrations: Vec::new(),
1214+
update_focus_chain: false,
11341215
}
11351216
}
11361217

1218+
pub(crate) fn is_disabled(&self) -> bool {
1219+
self.is_explicitly_disabled || self.ancestor_disabled
1220+
}
1221+
1222+
pub(crate) fn tree_disabled_changed(&self) -> bool {
1223+
self.children_disabled_changed
1224+
|| self.is_explicitly_disabled != self.is_explicitly_disabled_new
1225+
}
1226+
11371227
pub(crate) fn add_timer(&mut self, timer_token: TimerToken) {
11381228
self.timers.insert(timer_token, self.id);
11391229
}
@@ -1168,6 +1258,9 @@ impl WidgetState {
11681258

11691259
self.needs_layout |= child_state.needs_layout;
11701260
self.request_anim |= child_state.request_anim;
1261+
self.children_disabled_changed |= child_state.children_disabled_changed;
1262+
self.children_disabled_changed |=
1263+
child_state.is_explicitly_disabled_new != child_state.is_explicitly_disabled;
11711264
self.has_active |= child_state.has_active;
11721265
self.has_focus |= child_state.has_focus;
11731266
self.children_changed |= child_state.children_changed;
@@ -1176,6 +1269,7 @@ impl WidgetState {
11761269
self.timers.extend_drain(&mut child_state.timers);
11771270
self.text_registrations
11781271
.extend(child_state.text_registrations.drain(..));
1272+
self.update_focus_chain |= child_state.update_focus_chain;
11791273

11801274
// We reset `child_state.cursor` no matter what, so that on the every pass through the tree,
11811275
// things will be recalculated just from `cursor_change`.

druid/src/event.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,15 @@ pub enum LifeCycle {
265265
/// [`Size`]: struct.Size.html
266266
/// [`Widget::layout`]: trait.Widget.html#tymethod.layout
267267
Size(Size),
268+
/// Called when the Disabled state of the widgets is changed.
269+
///
270+
/// To check if a widget is disabled, see [`is_disabled`].
271+
///
272+
/// To change a widget's disabled state, see [`set_disabled`].
273+
///
274+
/// [`is_disabled`]: crate::EventCtx::is_disabled
275+
/// [`set_disabled`]: crate::EventCtx::set_disabled
276+
DisabledChanged(bool),
268277
/// Called when the "hot" status changes.
269278
///
270279
/// This will always be called _before_ the event that triggered it; that is,
@@ -274,6 +283,16 @@ pub enum LifeCycle {
274283
/// See [`is_hot`](struct.EventCtx.html#method.is_hot) for
275284
/// discussion about the hot status.
276285
HotChanged(bool),
286+
/// This is called when the widget-tree changes and druid wants to rebuild the
287+
/// Focus-chain.
288+
///
289+
/// It is the only place from witch [`register_for_focus`] should be called.
290+
/// By doing so the widget can get focused by other widgets using [`focus_next`] or [`focus_prev`].
291+
///
292+
/// [`register_for_focus`]: crate::LifeCycleCtx::register_for_focus
293+
/// [`focus_next`]: crate::EventCtx::focus_next
294+
/// [`focus_prev`]: crate::EventCtx::focus_prev
295+
BuildFocusChain,
277296
/// Called when the focus status changes.
278297
///
279298
/// This will always be called immediately after a new widget gains focus.
@@ -310,6 +329,8 @@ pub enum InternalLifeCycle {
310329
/// the widget that is gaining focus, if any
311330
new: Option<WidgetId>,
312331
},
332+
/// Used to route the `DisabledChanged` event to the required widgets.
333+
RouteDisabledChanged,
313334
/// The parents widget origin in window coordinate space has changed.
314335
ParentWindowOrigin,
315336
/// Testing only: request the `WidgetState` of a specific widget.
@@ -407,7 +428,9 @@ impl LifeCycle {
407428
pub fn should_propagate_to_hidden(&self) -> bool {
408429
match self {
409430
LifeCycle::Internal(internal) => internal.should_propagate_to_hidden(),
410-
LifeCycle::WidgetAdded => true,
431+
LifeCycle::WidgetAdded | LifeCycle::DisabledChanged(_) | LifeCycle::BuildFocusChain => {
432+
true
433+
}
411434
LifeCycle::Size(_) | LifeCycle::HotChanged(_) | LifeCycle::FocusChanged(_) => false,
412435
}
413436
}
@@ -418,9 +441,9 @@ impl InternalLifeCycle {
418441
/// (for example the hidden tabs in a tabs widget).
419442
pub fn should_propagate_to_hidden(&self) -> bool {
420443
match self {
421-
InternalLifeCycle::RouteWidgetAdded | InternalLifeCycle::RouteFocusChanged { .. } => {
422-
true
423-
}
444+
InternalLifeCycle::RouteWidgetAdded
445+
| InternalLifeCycle::RouteFocusChanged { .. }
446+
| InternalLifeCycle::RouteDisabledChanged => true,
424447
InternalLifeCycle::ParentWindowOrigin => false,
425448
#[cfg(test)]
426449
InternalLifeCycle::DebugRequestState { .. }

druid/src/tests/harness.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ impl<T: Data> Harness<'_, T> {
253253
}
254254
}
255255

256-
fn lifecycle(&mut self, event: LifeCycle) {
256+
pub(crate) fn lifecycle(&mut self, event: LifeCycle) {
257257
self.inner.lifecycle(event)
258258
}
259259

0 commit comments

Comments
 (0)