From 8c3a44deba096ca3488470a91812eba6e2a31c1d Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Sun, 26 Dec 2021 17:12:56 -0500 Subject: [PATCH] Stateful --- src/app.rs | 9 +- src/tuine/component/base/mod.rs | 2 +- .../component/base/text_table/builder.rs | 156 ++++++++++++ src/tuine/component/base/text_table/mod.rs | 236 +++++------------- .../component/base/text_table/sort_type.rs | 12 + src/tuine/component/mod.rs | 3 + src/tuine/component/stateful.rs | 14 ++ src/tuine/component/widget/temp_table.rs | 10 +- src/tuine/context/state_context.rs | 12 + src/tuine/context/state_map.rs | 40 +-- src/tuine/context/view_context.rs | 44 ++++ 11 files changed, 338 insertions(+), 200 deletions(-) create mode 100644 src/tuine/component/base/text_table/builder.rs create mode 100644 src/tuine/component/base/text_table/sort_type.rs create mode 100644 src/tuine/component/stateful.rs diff --git a/src/app.rs b/src/app.rs index d361cdb1..d058bf83 100644 --- a/src/app.rs +++ b/src/app.rs @@ -240,21 +240,22 @@ impl Application for AppState { fn view<'b>(&mut self, ctx: &mut ViewContext<'_>) -> Element { use crate::tuine::FlexElement; + use crate::tuine::StatefulTemplate; use crate::tuine::TempTable; - use crate::tuine::TextTable; + use crate::tuine::TextTableBuilder; Flex::column() .with_flex_child( Flex::row_with_children(vec![ FlexElement::new(TempTable::new(ctx)), - FlexElement::new(TextTable::new(ctx, vec!["D", "E", "F"])), + FlexElement::new(TextTableBuilder::new(vec!["D", "E", "F"]).build(ctx)), ]), 1, ) .with_flex_child( Flex::row_with_children(vec![ - FlexElement::new(TextTable::new(ctx, vec!["G", "H", "I", "J"])), - FlexElement::new(TextTable::new(ctx, vec!["K", "L", "M", "N"])), + FlexElement::new(TextTableBuilder::new(vec!["G", "H", "I", "J"]).build(ctx)), + FlexElement::new(TextTableBuilder::new(vec!["K", "L", "M", "N"]).build(ctx)), ]), 2, ) diff --git a/src/tuine/component/base/mod.rs b/src/tuine/component/base/mod.rs index 1255cf1c..ea1aeb11 100644 --- a/src/tuine/component/base/mod.rs +++ b/src/tuine/component/base/mod.rs @@ -1,5 +1,5 @@ pub mod text_table; -pub use text_table::{TextColumn, TextColumnConstraint, TextTable}; +pub use text_table::{TextColumn, TextColumnConstraint, TextTable, TextTableBuilder}; pub mod shortcut; pub use shortcut::Shortcut; diff --git a/src/tuine/component/base/text_table/builder.rs b/src/tuine/component/base/text_table/builder.rs new file mode 100644 index 00000000..63d08b3c --- /dev/null +++ b/src/tuine/component/base/text_table/builder.rs @@ -0,0 +1,156 @@ +use std::{borrow::Cow, panic::Location}; + +use crate::tuine::{StatefulTemplate, ViewContext}; + +use super::{DataRow, SortType, StyleSheet, TextColumn, TextTable, TextTableState}; + +pub struct TextTableBuilder { + column_widths: Vec, + columns: Vec, + show_gap: bool, + show_selected_entry: bool, + rows: Vec, + style_sheet: StyleSheet, + sort: SortType, + table_gap: u16, + on_select: Option Message>>, + on_selected_click: Option Message>>, +} + +impl TextTableBuilder { + pub fn new>>(columns: Vec) -> Self { + Self { + column_widths: vec![0; columns.len()], + columns: columns + .into_iter() + .map(|name| TextColumn::new(name)) + .collect(), + show_gap: true, + show_selected_entry: true, + rows: Vec::default(), + style_sheet: StyleSheet::default(), + sort: SortType::Unsortable, + table_gap: 0, + on_select: None, + on_selected_click: None, + } + } + + /// Sets the row to display in the table. + /// + /// Defaults to displaying no data if not set. + pub fn rows(mut self, rows: Vec) -> Self { + self.rows = rows; + self + } + + /// Adds a new row. + pub fn row(mut self, row: DataRow) -> Self { + self.rows.push(row); + self + } + + /// Whether to try to show a gap between the table headers and data. + /// Note that if there isn't enough room, the gap will still be hidden. + /// + /// Defaults to `true` if not set. + pub fn show_gap(mut self, show_gap: bool) -> Self { + self.show_gap = show_gap; + self + } + + /// Whether to highlight the selected entry. + /// + /// Defaults to `true` if not set. + pub fn show_selected_entry(mut self, show_selected_entry: bool) -> Self { + self.show_selected_entry = show_selected_entry; + self + } + + /// How the table should sort data on first initialization, if at all. + /// + /// Defaults to [`SortType::Unsortable`] if not set. + pub fn default_sort(mut self, sort: SortType) -> Self { + self.sort = sort; + self + } + + /// What to do when selecting an entry. Expects a boxed function that takes in + /// the currently selected index and returns a [`Message`]. + /// + /// Defaults to `None` if not set. + pub fn on_select(mut self, on_select: Option Message>>) -> Self { + self.on_select = on_select; + self + } + + /// What to do when clicking on an entry that is already selected. + /// + /// Defaults to `None` if not set. + pub fn on_selected_click( + mut self, on_selected_click: Option Message>>, + ) -> Self { + self.on_selected_click = on_selected_click; + self + } + + fn try_sort_data(&mut self, sort_type: SortType) { + use std::cmp::Ordering; + + // TODO: We can avoid some annoying checks by using const generics - this is waiting on + // the const_generics_defaults feature, landing in 1.59, however! + + fn sort_cmp(column: usize, a: &DataRow, b: &DataRow) -> Ordering { + match (a.get(column), b.get(column)) { + (Some(a), Some(b)) => a.cmp(b), + (Some(_a), None) => Ordering::Greater, + (None, Some(_b)) => Ordering::Less, + (None, None) => Ordering::Equal, + } + } + + match sort_type { + SortType::Ascending(column) => { + self.rows.sort_by(|a, b| sort_cmp(column, a, b)); + } + SortType::Descending(column) => { + self.rows.sort_by(|a, b| sort_cmp(column, a, b)); + self.rows.reverse(); + } + SortType::Unsortable => {} + } + } +} + +impl StatefulTemplate for TextTableBuilder { + type Component = TextTable; + type ComponentState = TextTableState; + + #[track_caller] + fn build(mut self, ctx: &mut ViewContext<'_>) -> Self::Component { + let sort = self.sort; + let (key, state) = ctx.register_and_mut_state_with_default::<_, Self::ComponentState, _>( + Location::caller(), + || TextTableState { + scroll: Default::default(), + sort, + }, + ); + + state.scroll.set_num_items(self.rows.len()); + self.try_sort_data(state.sort); + + TextTable { + key, + column_widths: self.column_widths, + columns: self.columns, + show_gap: self.show_gap, + show_selected_entry: self.show_selected_entry, + rows: self.rows, + style_sheet: self.style_sheet, + table_gap: self.table_gap, + on_select: self.on_select, + on_selected_click: self.on_selected_click, + } + } +} diff --git a/src/tuine/component/base/text_table/mod.rs b/src/tuine/component/base/text_table/mod.rs index 084f0656..229d5dc5 100644 --- a/src/tuine/component/base/text_table/mod.rs +++ b/src/tuine/component/base/text_table/mod.rs @@ -2,7 +2,7 @@ pub mod table_column; pub use self::table_column::{TextColumn, TextColumnConstraint}; mod table_scroll_state; -use self::table_scroll_state::ScrollState as TextTableState; +use self::table_scroll_state::ScrollState; pub mod data_row; pub use data_row::DataRow; @@ -10,7 +10,13 @@ pub use data_row::DataRow; pub mod data_cell; pub use data_cell::DataCell; -use std::{borrow::Cow, cmp::min, panic::Location}; +pub mod builder; +pub use builder::TextTableBuilder; + +pub mod sort_type; +pub use sort_type::SortType; + +use std::cmp::min; use tui::{ backend::Backend, @@ -23,7 +29,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::{ constants::TABLE_GAP_HEIGHT_LIMIT, - tuine::{DrawContext, Event, Key, StateContext, Status, TmpComponent, ViewContext}, + tuine::{DrawContext, Event, Key, StateContext, Status, TmpComponent}, }; #[derive(Clone, Debug, Default)] @@ -33,9 +39,10 @@ pub struct StyleSheet { table_header: Style, } -enum SortStatus { - Unsortable, - Sortable { column: usize, reverse: bool }, +#[derive(PartialEq, Default)] +pub struct TextTableState { + scroll: ScrollState, + sort: SortType, } /// A sortable, scrollable table for text data. @@ -47,152 +54,12 @@ pub struct TextTable { show_selected_entry: bool, rows: Vec, style_sheet: StyleSheet, - sortable: SortStatus, // FIXME: Should this be stored in state? table_gap: u16, on_select: Option Message>>, on_selected_click: Option Message>>, } impl TextTable { - #[track_caller] - pub fn new>>(ctx: &mut ViewContext<'_>, columns: Vec) -> Self { - Self { - key: ctx.register_component(Location::caller()), - column_widths: vec![0; columns.len()], - columns: columns - .into_iter() - .map(|name| TextColumn::new(name)) - .collect(), - show_gap: true, - show_selected_entry: true, - rows: Vec::default(), - style_sheet: StyleSheet::default(), - sortable: SortStatus::Unsortable, - table_gap: 0, - on_select: None, - on_selected_click: None, - } - } - - /// Sets the row to display in the table. - /// - /// Defaults to displaying no data if not set. - pub fn rows(mut self, rows: Vec) -> Self { - self.rows = rows; - self.try_sort_data(); - - self - } - - /// Adds a new row. - pub fn row(mut self, row: DataRow) -> Self { - self.rows.push(row); - self.try_sort_data(); - - self - } - - /// Whether to try to show a gap between the table headers and data. - /// Note that if there isn't enough room, the gap will still be hidden. - /// - /// Defaults to `true` if not set. - pub fn show_gap(mut self, show_gap: bool) -> Self { - self.show_gap = show_gap; - self - } - - /// Whether to highlight the selected entry. - /// - /// Defaults to `true` if not set. - pub fn show_selected_entry(mut self, show_selected_entry: bool) -> Self { - self.show_selected_entry = show_selected_entry; - self - } - - /// Whether the table should display as sortable. - /// - /// Defaults to unsortable if not set. - pub fn sortable(mut self, sortable: bool) -> Self { - self.sortable = if sortable { - SortStatus::Sortable { - column: 0, - reverse: false, - } - } else { - SortStatus::Unsortable - }; - self.try_sort_data(); - self - } - - /// Calling this enables sorting, and sets the sort column to `column`. - pub fn sort_column(mut self, column: usize) -> Self { - self.sortable = match self.sortable { - SortStatus::Unsortable => SortStatus::Sortable { - column, - reverse: false, - }, - SortStatus::Sortable { column: _, reverse } => SortStatus::Sortable { column, reverse }, - }; - self.try_sort_data(); - self - } - - /// Calling this enables sorting, and sets the reverse status to `reverse`. - pub fn sort_reverse(mut self, reverse: bool) -> Self { - self.sortable = match self.sortable { - SortStatus::Unsortable => SortStatus::Sortable { column: 0, reverse }, - SortStatus::Sortable { column, reverse: _ } => SortStatus::Sortable { column, reverse }, - }; - self.try_sort_data(); - self - } - - /// Returns whether the table is currently sortable. - pub fn is_sortable(&self) -> bool { - matches!(self.sortable, SortStatus::Sortable { .. }) - } - - /// What to do when selecting an entry. Expects a boxed function that takes in - /// the currently selected index and returns a [`Message`]. - /// - /// Defaults to `None` if not set. - pub fn on_select(mut self, on_select: Option Message>>) -> Self { - self.on_select = on_select; - self - } - - /// What to do when clicking on an entry that is already selected. - /// - /// Defaults to `None` if not set. - pub fn on_selected_click( - mut self, on_selected_click: Option Message>>, - ) -> Self { - self.on_selected_click = on_selected_click; - self - } - - fn try_sort_data(&mut self) { - use std::cmp::Ordering; - - if let SortStatus::Sortable { column, reverse } = self.sortable { - // TODO: We can avoid some annoying checks by using const generics - this is waiting on - // the const_generics_defaults feature, landing in 1.59, however! - - self.rows - .sort_by(|a, b| match (a.get(column), b.get(column)) { - (Some(a), Some(b)) => a.cmp(b), - (Some(_a), None) => Ordering::Greater, - (None, Some(_b)) => Ordering::Less, - (None, None) => Ordering::Equal, - }); - - if reverse { - self.rows.reverse(); - } - } - } - fn update_column_widths(&mut self, bounds: Rect) { let total_width = bounds.width; let mut width_remaining = bounds.width; @@ -251,7 +118,7 @@ impl TmpComponent for TextTable { { let rect = draw_ctx.rect(); let state = state_ctx.mut_state::(self.key); - state.set_num_items(self.rows.len()); // FIXME: Not a fan of this system like this - should be easier to do. + state.scroll.set_num_items(self.rows.len()); // FIXME: Not a fan of this system like this - should be easier to do. self.table_gap = if !self.show_gap || (self.rows.len() + 2 > rect.height.into() && rect.height < TABLE_GAP_HEIGHT_LIMIT) @@ -276,8 +143,10 @@ impl TmpComponent for TextTable { // as well as truncating some entries based on available width. let data_slice = { // Note: `get_list_start` already ensures `start` is within the bounds of the number of items, so no need to check! - let start = state.display_start_index(rect, scrollable_height as usize); - let end = min(state.num_items(), start + scrollable_height as usize); + let start = state + .scroll + .display_start_index(rect, scrollable_height as usize); + let end = min(state.scroll.num_items(), start + scrollable_height as usize); debug!("Start: {}, end: {}", start, end); self.rows.drain(start..end).into_iter().map(|row| { @@ -299,7 +168,7 @@ impl TmpComponent for TextTable { table = table.highlight_style(self.style_sheet.selected_text); } - frame.render_stateful_widget(table.widths(&widths), rect, state.tui_state()); + frame.render_stateful_widget(table.widths(&widths), rect, state.scroll.tui_state()); } fn on_event( @@ -311,7 +180,6 @@ impl TmpComponent for TextTable { let rect = draw_ctx.rect(); let state = state_ctx.mut_state::(self.key); - state.set_num_items(self.rows.len()); match event { Event::Keyboard(key_event) => { @@ -330,30 +198,37 @@ impl TmpComponent for TextTable { let y = mouse_event.row - rect.top(); if y == 0 { - if let SortStatus::Sortable { column, reverse } = self.sortable { - todo!() // Sort by the clicked column! If already using column, reverse! - // self.sort_data(); - } else { - Status::Ignored + match state.sort { + SortType::Unsortable => Status::Ignored, + SortType::Ascending(column) => { + // Sort by the clicked column! If already using column, reverse! + // self.sort_data(); + todo!() + } + SortType::Descending(column) => { + // Sort by the clicked column! If already using column, reverse! + // self.sort_data(); + todo!() + } } } else if y > self.table_gap { let visual_index = usize::from(y - self.table_gap); - state.set_visual_index(visual_index) + state.scroll.set_visual_index(visual_index) } else { Status::Ignored } } MouseEventKind::ScrollDown => { - let status = state.move_down(1); + let status = state.scroll.move_down(1); if let Some(on_select) = &self.on_select { - messages.push(on_select(state.current_index())); + messages.push(on_select(state.scroll.current_index())); } status } MouseEventKind::ScrollUp => { - let status = state.move_up(1); + let status = state.scroll.move_up(1); if let Some(on_select) = &self.on_select { - messages.push(on_select(state.current_index())); + messages.push(on_select(state.scroll.current_index())); } status } @@ -369,7 +244,10 @@ impl TmpComponent for TextTable { #[cfg(test)] mod tests { - use crate::tuine::{StateMap, ViewContext}; + use crate::tuine::{ + text_table::{SortType, TextTableBuilder}, + StateMap, StatefulTemplate, ViewContext, + }; use super::{DataRow, TextTable}; @@ -390,9 +268,10 @@ mod tests { let index = 1; let mut map = StateMap::default(); - let table: TextTable = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"]) - .sort_column(index) - .rows(rows); + let table: TextTable = TextTableBuilder::new(vec!["Sensor", "Temp"]) + .default_sort(SortType::Ascending(index)) + .rows(rows) + .build(&mut ctx(&mut map)); assert_eq!( table.rows.len(), @@ -422,10 +301,11 @@ mod tests { let new_index = 0; let mut map = StateMap::default(); - let table: TextTable = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"]) - .sort_column(index) + let table: TextTable = TextTableBuilder::new(vec!["Sensor", "Temp"]) + .default_sort(SortType::Ascending(index)) .rows(rows) - .sort_column(new_index); + .default_sort(SortType::Ascending(new_index)) + .build(&mut ctx(&mut map)); assert_eq!( table.rows.len(), @@ -454,10 +334,10 @@ mod tests { let index = 1; let mut map = StateMap::default(); - let table: TextTable = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"]) - .sort_column(index) - .sort_reverse(true) - .rows(rows); + let table: TextTable = TextTableBuilder::new(vec!["Sensor", "Temp"]) + .default_sort(SortType::Descending(index)) + .rows(rows) + .build(&mut ctx(&mut map)); assert_eq!( table.rows.len(), @@ -486,10 +366,11 @@ mod tests { let index = 1; let mut map = StateMap::default(); - let table: TextTable = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"]) + let table: TextTable = TextTableBuilder::new(vec!["Sensor", "Temp"]) .rows(rows) - .sort_column(index) - .row(DataRow::default().cell("X").cell(0)); + .default_sort(SortType::Ascending(index)) + .row(DataRow::default().cell("X").cell(0)) + .build(&mut ctx(&mut map)); assert_eq!( table.rows.len(), @@ -519,9 +400,10 @@ mod tests { let row_length = original_rows.len(); let mut map = StateMap::default(); - let table: TextTable = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"]) + let table: TextTable = TextTableBuilder::new(vec!["Sensor", "Temp"]) .rows(rows) - .row(original_rows[3].clone()); + .row(original_rows[3].clone()) + .build(&mut ctx(&mut map)); assert_eq!( table.rows.len(), diff --git a/src/tuine/component/base/text_table/sort_type.rs b/src/tuine/component/base/text_table/sort_type.rs new file mode 100644 index 00000000..ed5fc355 --- /dev/null +++ b/src/tuine/component/base/text_table/sort_type.rs @@ -0,0 +1,12 @@ +#[derive(Clone, Copy, PartialEq)] +pub enum SortType { + Unsortable, + Ascending(usize), + Descending(usize), +} + +impl Default for SortType { + fn default() -> Self { + Self::Unsortable + } +} diff --git a/src/tuine/component/mod.rs b/src/tuine/component/mod.rs index eee19539..8d637635 100644 --- a/src/tuine/component/mod.rs +++ b/src/tuine/component/mod.rs @@ -4,6 +4,9 @@ pub use base::*; pub mod widget; pub use widget::*; +pub mod stateful; +pub use stateful::*; + use enum_dispatch::enum_dispatch; use tui::Frame; diff --git a/src/tuine/component/stateful.rs b/src/tuine/component/stateful.rs new file mode 100644 index 00000000..f7711aa5 --- /dev/null +++ b/src/tuine/component/stateful.rs @@ -0,0 +1,14 @@ +use crate::tuine::{State, ViewContext}; + +use super::TmpComponent; + +/// A [`StatefulTemplate`] is a builder-style pattern for building a stateful +/// [`Component`]. +/// +/// Inspired by Flutter's StatefulWidget interface. +pub trait StatefulTemplate { + type Component: TmpComponent; + type ComponentState: State; + + fn build(self, ctx: &mut ViewContext<'_>) -> Self::Component; +} diff --git a/src/tuine/component/widget/temp_table.rs b/src/tuine/component/widget/temp_table.rs index dda80307..91eaf04b 100644 --- a/src/tuine/component/widget/temp_table.rs +++ b/src/tuine/component/widget/temp_table.rs @@ -1,4 +1,7 @@ -use crate::tuine::{text_table::DataRow, Shortcut, TextTable, TmpComponent, ViewContext}; +use crate::tuine::{ + text_table::{DataRow, SortType, TextTableBuilder}, + Shortcut, StatefulTemplate, TextTable, TmpComponent, ViewContext, +}; /// A [`TempTable`] is a text table that is meant to display temperature data. pub struct TempTable { @@ -10,13 +13,14 @@ impl TempTable { pub fn new(ctx: &mut ViewContext<'_>) -> Self { Self { inner: Shortcut::with_child( - TextTable::new(ctx, vec!["Sensor", "Temp"]) + TextTableBuilder::new(vec!["Sensor", "Temp"]) .rows(vec![ DataRow::default().cell("A").cell(2), DataRow::default().cell("B").cell(3), DataRow::default().cell("C").cell(1), ]) - .sort_column(1), + .default_sort(SortType::Ascending(1)) + .build(ctx), ), } } diff --git a/src/tuine/context/state_context.rs b/src/tuine/context/state_context.rs index 8ee1dd4f..c22f5f75 100644 --- a/src/tuine/context/state_context.rs +++ b/src/tuine/context/state_context.rs @@ -16,4 +16,16 @@ impl<'a> StateContext<'a> { pub fn mut_state(&mut self, key: Key) -> &mut S { self.state_map.mut_state::(key) } + + pub fn state_with_default S>( + &mut self, key: Key, default: F, + ) -> &S { + self.state_map.state_with_default::(key, default) + } + + pub fn mut_state_with_default S>( + &mut self, key: Key, default: F, + ) -> &mut S { + self.state_map.mut_state_with_default::(key, default) + } } diff --git a/src/tuine/context/state_map.rs b/src/tuine/context/state_map.rs index 8d0cb241..a09ee92c 100644 --- a/src/tuine/context/state_map.rs +++ b/src/tuine/context/state_map.rs @@ -3,34 +3,44 @@ use rustc_hash::FxHashMap; use crate::tuine::{Key, State}; #[derive(Default)] -pub struct StateMap(FxHashMap, bool)>); +pub struct StateMap(FxHashMap>); impl StateMap { pub fn state(&mut self, key: Key) -> &S { - let state = self - .0 - .entry(key) - .or_insert_with(|| (Box::new(S::default()), true)); - - state.1 = true; + let state = self.0.entry(key).or_insert_with(|| Box::new(S::default())); state - .0 .as_any() .downcast_ref() .expect("Successful downcast of state.") } pub fn mut_state(&mut self, key: Key) -> &mut S { - let state = self - .0 - .entry(key) - .or_insert_with(|| (Box::new(S::default()), true)); - - state.1 = true; + let state = self.0.entry(key).or_insert_with(|| Box::new(S::default())); + + state + .as_mut_any() + .downcast_mut() + .expect("Successful downcast of state.") + } + + pub fn state_with_default S>( + &mut self, key: Key, default: F, + ) -> &S { + let state = self.0.entry(key).or_insert_with(|| Box::new(default())); + + state + .as_any() + .downcast_ref() + .expect("Successful downcast of state.") + } + + pub fn mut_state_with_default S>( + &mut self, key: Key, default: F, + ) -> &mut S { + let state = self.0.entry(key).or_insert_with(|| Box::new(default())); state - .0 .as_mut_any() .downcast_mut() .expect("Successful downcast of state.") diff --git a/src/tuine/context/view_context.rs b/src/tuine/context/view_context.rs index fccea752..ddcc45ac 100644 --- a/src/tuine/context/view_context.rs +++ b/src/tuine/context/view_context.rs @@ -27,4 +27,48 @@ impl<'a> ViewContext<'a> { pub fn mut_state(&mut self, key: Key) -> &mut S { self.state_context.mut_state(key) } + + pub fn register_and_state, S: State + Default + 'static>( + &mut self, caller: C, + ) -> (Key, &S) { + self.key_counter += 1; + let key = Key::new(caller.into(), self.key_counter); + + (key, self.state(key)) + } + + pub fn register_and_mut_state, S: State + Default + 'static>( + &mut self, caller: C, + ) -> (Key, &mut S) { + self.key_counter += 1; + let key = Key::new(caller.into(), self.key_counter); + + (key, self.mut_state(key)) + } + + pub fn register_and_state_with_default< + C: Into, + S: State + 'static, + F: FnOnce() -> S, + >( + &mut self, caller: C, default: F, + ) -> (Key, &S) { + self.key_counter += 1; + let key = Key::new(caller.into(), self.key_counter); + + (key, self.state_context.state_with_default(key, default)) + } + + pub fn register_and_mut_state_with_default< + C: Into, + S: State + 'static, + F: FnOnce() -> S, + >( + &mut self, caller: C, default: F, + ) -> (Key, &mut S) { + self.key_counter += 1; + let key = Key::new(caller.into(), self.key_counter); + + (key, self.state_context.mut_state_with_default(key, default)) + } }