#Conceptual problem regarding a stateful UI framework

2 messages · Page 1 of 1 (latest)

rugged nymph
#

I'm trying to create an abstract GUI library in Rust for building GUI applications.
The library has the types and concepts of Components, Actions, Focus, Text/Mouse Input.
To make components abstract and reusable, I'm using Capabilities (Box<dyn Any + Send + Sync>) + downcasting.
Along with that, to manage state, I'm using the same downcasting approach to be able to store the states in a generic way.
Here is the current structure:

UIState
 ├── Components
 │    ├── TextInput
 │    │    └── Capabilities<TextInput>
 │    └── CheckBox
 │         └── Capabilities<CheckBox>
 └── ComponentStates
      ├── State<TextInput>
      └── State<CheckBox>

The problem I'm facing is that some of the components have Capabilities, which contain callbacks that need to access the component's state.
For example, a Checkbox component has a MouseInputCapability with a callback that toggles the checked state of the checkbox.
The same can be naturally said about other things like TextInput where the KeyboardInputCapability needs to modify the text state.

The issue I'm breaking my head around is if I should somehow change my object model completely to accommodate this, because I'm not the biggest fan of having to wrap every state in an Arc<Mutex<>> to be able to share it with the capability callbacks, mainly due to performance concerns.

Just in case it's relevant, I'm using ratatui as a test for my abstractions.

#

Here are some relevant code snippets:

pub struct UIState {
    components: SlotMap<Glyph, UINode>,
    root: Glyph,
    glyph_states: HashMap<Glyph, Arc<Mutex<GlyphState>>>,
}
pub struct UINode {
    component: Component,
    children: Vec<Glyph>,
    parent: Option<Glyph>,
    capabilities: Vec<Capability>,
}
pub trait Capabilities<T: 'static>: Send + Sync {
    fn init_capabilities(&self, state: &GlyphStateMutex<T>) -> Vec<Capability>;
}
pub type Capability = Box<dyn Any + Send + Sync>;
pub type GlyphState = dyn Any + Send + Sync;
pub type GlyphStateMutex<T> = Arc<Mutex<T>>;
pub trait Stateful<T: 'static>: Send + Sync {
    fn init_state(&self) -> GlyphStateMutex<T>;
    fn get_state<'a>(state: &'a GlyphState) -> Option<&'a T> {
        state.downcast_ref::<T>()
    }
    fn get_state_mut<'a>(state: &'a mut GlyphState) -> Option<&'a mut T> {
        state.downcast_mut::<T>()
    }
}
pub struct MouseInputFunctionality {
    pub callback: Box<dyn Fn(MouseInputEvent) -> Pin<Box<dyn Future<Output = bool> + Send>> + Send + Sync>,
}
pub enum Component {
    Window(Window),
    Panel(Panel),
    Label(Label),
    TextInput(TextInput),
    Button(Button),
    Checkbox(Checkbox),
    RadioGroup(RadioGroup),
    Radio(Radio),
    Dropdown(Dropdown),
    Grid(Grid),
    Column(Column),
    Modal(Modal),
    Tabs(Tabs),
    Tab(Tab),
    Custom(CustomComponent),
}
pub struct Checkbox {
    pub title: String,
}
impl Checkbox {
    pub fn new(title: String) -> (Self, GlyphStateMutex<CheckBoxState>) {
        (
            Checkbox { title },
            Self::init_state(),
        )
    }
}
impl Capabilities<CheckBoxState> for Checkbox {
    fn init_capabilities(&self, state: &GlyphStateMutex<CheckBoxState>) -> Vec<Capability> {
        let state_for_mouse = Arc::clone(state);
        vec![
            Box::new(RenderFunctionality {}),
            Box::new(FocusFunctionality {}),
            Box::new(MouseInputFunctionality {
                callback: Box::new(move |event| {
                    let state = Arc::clone(&state_for_mouse);
                    Box::pin(async move {
                        if event.button == MouseButton::Left {
                            if let Ok(mut state) = state.lock() {
                                state.checked = !state.checked;
                            }
                            true
                        } else {
                            false
                        }
                    })
                }),
            }),
            ...
        ]
    }
}
impl Stateful<CheckBoxState> for Checkbox {
    fn init_state(&self) -> GlyphStateMutex<CheckBoxState> {
        Arc::new(Mutex::new(CheckBoxState {
            checked: false,
            on_change: None,
        }))
    }
}
pub struct CheckBoxState {
    pub checked: bool,
    pub on_change: Option<CheckboxChangeCallback>,
    // possibly other fields like style, disabled state, etc.
}