Skip to content

Commit

Permalink
Expose scope state (#2082)
Browse files Browse the repository at this point in the history
* Expose scope state.

This is mainly for composite widgets that contain a scope: using this they can affect the widgets inside the scope.
Using this facilty, allow getting and setting the tab index of a Tabs widget.
This offers part of a solution to #1390, although it is in terms of the index and not the key.

* Add suggestions from review

Co-authored-by: Robert Wittams <robert@wittams.com>
  • Loading branch information
maan2003 and rjwittams authored Dec 30, 2021
1 parent 004c6eb commit ab6af4e
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 14 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ You can find its changes [documented below](#070---2021-01-01).
- `scroll_to_view` and `scroll_area_to_view` methods on `UpdateCtx`, `LifecycleCtx` and `EventCtx` ([#1976] by [@xarvic])
- `Notification::route` ([#1978] by [@xarvic])
- Build on OpenBSD ([#1993] by [@klemensn])
- Scope: expose scoped state using state() and state_mut() ([#2082] by [@rjwittams]
- Tabs: allow getting and setting the tab index of a Tabs widget ([#2082] by [@rjwittams]

### Changed

Expand Down
3 changes: 2 additions & 1 deletion druid/examples/tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ fn build_tab_widget(tab_config: &TabConfig) -> impl Widget<AppState> {
.with_tab("Page 3", Label::new("Page 3 content"))
.with_tab("Page 4", Label::new("Page 4 content"))
.with_tab("Page 5", Label::new("Page 5 content"))
.with_tab("Page 6", Label::new("Page 6 content"));
.with_tab("Page 6", Label::new("Page 6 content"))
.with_tab_index(1);

Split::rows(main_tabs, dyn_tabs).draggable(true)
}
36 changes: 32 additions & 4 deletions druid/src/widget/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ pub trait ScopeTransfer {
type State: Data;

/// Replace the input we have within our State with a new one from outside
fn read_input(&self, state: &mut Self::State, inner: &Self::In);
fn read_input(&self, state: &mut Self::State, input: &Self::In);
/// Take the modifications we have made and write them back
/// to our input.
fn write_back_input(&self, state: &Self::State, inner: &mut Self::In);
fn write_back_input(&self, state: &Self::State, input: &mut Self::In);
}

/// A default implementation of [`ScopePolicy`] that takes a function and a transfer.
Expand Down Expand Up @@ -84,8 +84,8 @@ impl<F: FnOnce(Transfer::In) -> Transfer::State, Transfer: ScopeTransfer> ScopeP
type State = Transfer::State;
type Transfer = Transfer;

fn create(self, inner: &Self::In) -> (Self::State, Self::Transfer) {
let state = (self.make_state)(inner.clone());
fn create(self, input: &Self::In) -> (Self::State, Self::Transfer) {
let state = (self.make_state)(input.clone());
(state, self.transfer)
}
}
Expand Down Expand Up @@ -215,6 +215,34 @@ impl<SP: ScopePolicy, W: Widget<SP::State>> Scope<SP, W> {
}
}

/// A reference to the contents of the `Scope`'s state.
///
/// This allows you to access the content from outside the widget.
pub fn state(&self) -> Option<&SP::State> {
if let ScopeContent::Transfer { ref state, .. } = &self.content {
Some(state)
} else {
None
}
}

/// A mutable reference to the contents of the [`Scope`]'s state.
///
/// This allows you to mutably access the content of the `Scope`' s state from
/// outside the widget. Mainly useful for composite widgets.
///
/// # Note:
///
/// If you modify the state through this reference, the Scope will not
/// call update on its children until the next event it receives.
pub fn state_mut(&mut self) -> Option<&mut SP::State> {
if let ScopeContent::Transfer { ref mut state, .. } = &mut self.content {
Some(state)
} else {
None
}
}

fn with_state<V>(
&mut self,
data: &SP::In,
Expand Down
64 changes: 55 additions & 9 deletions druid/src/widget/tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,14 @@ impl<TP: TabsPolicy> Widget<TabsState<TP>> for TabsBody<TP> {
}

if let (Some(t_state), Event::AnimFrame(interval)) = (&mut self.transition_state, event) {
t_state.current_time += *interval;
// We can get a high interval on the first frame due to other widgets or old animations.
let interval = if t_state.current_time == 0 {
1
} else {
*interval
};

t_state.current_time += interval;
if t_state.live() {
ctx.request_anim_frame();
} else {
Expand Down Expand Up @@ -794,9 +801,11 @@ impl<T: Data> InitialTab<T> {
enum TabsContent<TP: TabsPolicy> {
Building {
tabs: TP::Build,
index: TabIndex,
},
Complete {
tabs: TP,
index: TabIndex,
},
Running {
scope: WidgetPod<TP::Input, TabsScope<TP>>,
Expand Down Expand Up @@ -862,7 +871,7 @@ impl<TP: TabsPolicy> Tabs<TP> {
/// Create a Tabs widget using the provided policy.
/// This is useful for tabs derived from data.
pub fn for_policy(tabs: TP) -> Self {
Self::of_content(TabsContent::Complete { tabs })
Self::of_content(TabsContent::Complete { tabs, index: 0 })
}

// This could be public if there is a case for custom policies that support static tabs - ie the AddTab method.
Expand All @@ -874,6 +883,7 @@ impl<TP: TabsPolicy> Tabs<TP> {
{
Self::of_content(TabsContent::Building {
tabs: tabs_from_data,
index: 0,
})
}

Expand Down Expand Up @@ -909,6 +919,12 @@ impl<TP: TabsPolicy> Tabs<TP> {
self
}

/// A builder-style method to specify the (zero-based) index of the selected tab.
pub fn with_tab_index(mut self, idx: TabIndex) -> Self {
self.set_tab_index(idx);
self
}

/// Available when the policy implements AddTab - e.g StaticTabs.
/// Return this Tabs widget with the named tab added.
pub fn add_tab(
Expand All @@ -918,14 +934,44 @@ impl<TP: TabsPolicy> Tabs<TP> {
) where
TP: AddTab,
{
if let TabsContent::Building { tabs } = &mut self.content {
if let TabsContent::Building { tabs, .. } = &mut self.content {
TP::add_tab(tabs, name, child)
} else {
tracing::warn!("Can't add static tabs to a running or complete tabs instance!")
}
}

fn make_scope(&self, tabs_from_data: TP) -> WidgetPod<TP::Input, TabsScope<TP>> {
/// The (zero-based) index of the currently selected tab.
pub fn tab_index(&self) -> TabIndex {
let index = match &self.content {
TabsContent::Running { scope, .. } => scope.widget().state().map(|s| s.selected),
TabsContent::Building { index, .. } | TabsContent::Complete { index, .. } => {
Some(*index)
}
TabsContent::Swapping => None,
};
index.unwrap_or(0)
}

/// Set the selected (zero-based) tab index.
///
/// This tab will become visible if it exists. If animations are enabled
/// (and the widget is laid out), the tab transition will be animated.
pub fn set_tab_index(&mut self, idx: TabIndex) {
match &mut self.content {
TabsContent::Running { scope, .. } => {
if let Some(state) = scope.widget_mut().state_mut() {
state.selected = idx
}
}
TabsContent::Building { index, .. } | TabsContent::Complete { index, .. } => {
*index = idx;
}
TabsContent::Swapping => (),
}
}

fn make_scope(&self, tabs_from_data: TP, idx: TabIndex) -> WidgetPod<TP::Input, TabsScope<TP>> {
let tabs_bar = TabBar::new(self.axis, self.edge);
let tabs_body = TabsBody::new(self.axis, self.transition)
.padding(5.)
Expand All @@ -941,7 +987,7 @@ impl<TP: TabsPolicy> Tabs<TP> {
};

WidgetPod::new(Scope::new(
TabsScopePolicy::new(tabs_from_data, 0),
TabsScopePolicy::new(tabs_from_data, idx),
Box::new(layout),
))
}
Expand All @@ -967,16 +1013,16 @@ impl<TP: TabsPolicy> Widget<TP::Input> for Tabs<TP> {
let content = std::mem::replace(&mut self.content, TabsContent::Swapping);

self.content = match content {
TabsContent::Building { tabs } => {
TabsContent::Building { tabs, index } => {
ctx.children_changed();
TabsContent::Running {
scope: self.make_scope(TP::build(tabs)),
scope: self.make_scope(TP::build(tabs), index),
}
}
TabsContent::Complete { tabs } => {
TabsContent::Complete { tabs, index } => {
ctx.children_changed();
TabsContent::Running {
scope: self.make_scope(tabs),
scope: self.make_scope(tabs, index),
}
}
_ => content,
Expand Down

0 comments on commit ab6af4e

Please sign in to comment.