lapce / floem

A native Rust UI library with fine-grained reactivity
https://lap.dev/floem/
MIT License
3.06k stars 133 forks source link

add derive macro for generating state structs #649

Open jrmoulton opened 3 weeks ago

jrmoulton commented 3 weeks ago

A little proc macro to make it easier to model data as items that can be used in the UI

(would definitely make it conditional on a flag)

I think this is a nice convenient macro that encourages a good separation of regular application data and data that is to be displayed in a UI but I'm on the fence about including this.

Example of how this would be used:

#[derive(Clone, floem::State)]
#[state_derives(Clone, Copy, Eq, Debug)]
/// There is a `Todo` struct that doesn't have any signals or anything to do with the UI and could be built and used separately from the UI.
pub struct Todo {
    pub db_id: Option<i64>,
    #[state_skip]
    pub unique_id: u64,
    pub done: bool,
    pub description: String,
}
static UNIQUE_COUNTER: AtomicU64 = AtomicU64::new(0);
impl Todo {
    pub fn new_from_db(db_id: i64, done: bool, description: impl Into<String>) -> Self {
        Self {
            db_id: Some(db_id),
            unique_id: UNIQUE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
            done,
            description: description.into(),
        }
    }
    pub fn new(done: bool, description: impl Into<String>) -> Self {
        Self {
            db_id: None,
            unique_id: UNIQUE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
            done,
            description: description.into(),
        }
    }
}

/// The `TodoState` struct is generated and can be created by doing `todo.to_state()` and simply wraps the fields of `Todo` in signals and makes it easy to impl `IntoView`. 
///
/// This really isn't incredibly useful but it is nice, encourages a good separation, and removes a tedious task of manually creating signals.
impl IntoView for TodoState {
    type V = AnyView;

    fn into_view(self) -> Self::V {

        // At this point the `db_id`, `done`, and `description` are signals 
        // and are ready to be used for values that change over time in the UI.

        debounce_action(self.done, 300.millis(), move || {
            AppCommand::UpdateDone(self).execute()
        });

        debounce_action(self.description, 300.millis(), move || {
            AppCommand::UpdateDescription(self).execute()
        });

        let state = todo_state();
        let is_selected = create_memo(move |_| state.selected.with(|s| s.contains(&self)));
        let is_active =
            create_memo(move |_| state.active.with(|s| s.active.map_or(false, |v| v == self)));

        let input_focused = Trigger::new();
        let done_check = Checkbox::new_rw(self.done)
            .style(|s| {
                ...
            });

        let input = todo_input(self).into_view();
        let input_id = input.id();
        let input = input
            .disable_default_event(move || (EventListener::PointerDown, !is_active))
            .style(move |s| {
                ...
            });

        let main_controls = (done_check, input)
            .h_stack()
            .debug_name("Todo Checkbox and text input (main controls)")
            .style(|s| s.gap(10).width_full().items_center())
            .container()
            .style(|s| s.width_full().align_items(Some(AlignItems::FlexStart)));

        let container = main_controls.container();

        container
            .style(move |s| {
                ...
            })
            .into_any()
    }
}