React Hooks like API for enhancing the ergonomics of egui::Memory
| egui version | egui_hooks version |
|---|---|
| [email protected] | 0.1.2 |
| [email protected] | 0.2.0 |
| [email protected] | 0.3.0 |
| [email protected] | 0.4.0 |
| [email protected] | 0.5.0 |
| [email protected] | 0.6.0 |
| [email protected] | 0.7.0 |
| [email protected] | 0.8.0 |
| [email protected] | 0.9.0 |
| [email protected] | 0.10.0 |
This crate provids React Hooks like API for egui.
Though this started as a toy project, eventually I found that it's definitely useful and that could be a core building block for widget development, and also for application development.
Look at the examples, run
cargo run --example <example-name> to see the demo.
- No resource leak: Opposite to using
egui::Memorydirectly, the states are automatically freed from the HashMap when the widget will be no longer displayed. This is based onTwoFrameMap(2f kv) defined in this crate. - No locking nor callback: You can manage states without
ui.data(|| { ... }). This is because hooks encapsulate the underlying RwLock operation. - Dependency tracking: Hooks has dependencies like
use_state(|| user_id.clone(), user_id)oruse_effect(|| log(input), input), so you can precisely track the dependencies without manually writingifstatements on state change. - Composable: Hooks are composable, you can call existing hooks in your custom hooks.
- Familiar API: Hooks are designed to be similar to React Hooks API, so you can
easily learn how to use them. Managing UI states in UI side is the key in
recent UI development scene, but built-in
egui::Memoryis relatively low-level API and not friendly for applcation development, and egui_hooks provides a higher level API but with more precise control.
If you use use_state(|| 0usize, dep).into_var() in a widget, the following
things happen:
- On the first call of
use_state, it creates aArc<ArcSwap<usize>>in theegui::Memorywith the default value. - If the
depis changed since the last frame, it stores the default value to the existingArcSwap. - Returns a
Var<usize>to the caller. - Caller can
DereforDerefMuttheVarin their widget code. - When the
Varis dropped, it stores the updated value to theArcSwap. - Wenn the widget is no longer displayed, the
ArcSwapis removed from theegui::Memory.
This is the typical lifecycle of a hook in egui_hooks.
Also, there is a persistent version of use_state called use_persisted_state.
It does the similar thing, but it stores the copy of the state to the
egui::Memory with persisted methods. The persisted state is freed when the
widget is no longer displayed as like the not-persisted one. You need persistence feature to use persisted hooks.
use_statefor states in a specific widget (e.g. animation state, scroll position)use_statewithinto_var()to feed a variable in-place toWindow::openorTextEdit::singlelineuse_memo,use_cachefor caching expensive calculationuse_effect,use_futurefor side effects (e.g. logging, network request)use_globalfor global settings (e.g. theme, locale)use_kvfor sharing states between widgets (e.g. getting a position of a specific widget)use_ephemeral_kvfor storing events in the current frame (e.g. providing custom response on a custom widget)use_previous_measurementfor using the previous frame result for layoutinguse_measurementfor calculating and memoizing the size of a widget for layouting
-
use_memo -
use_effect -
use_effect_with_cleanup -
use_state,use_persisted_state -
state.into_var()to use state as a variable -
use_kv,use_persisted_kv -
use_2f_kv,use_persisted_2f_kv -
use_ephemeral_kv -
use_global,use_persisted_global, anduse_ephemeral_global -
use_cache(a thin wrapper of caches inegui::Memory) -
use_previous_measurement -
use_measurement(calculate the size of the widget without fear of the 2^N problem. -
use_future(needstokiofeature) -
use_throttleanduse_debounce -
use_drag_origin -
use_two_path(it's joke, but really want to implement this)
// You can reset the initial state by changing the dependency part.
let count = ui.use_state(|| 0usize, ());
ui.label(format!("Count: {}", count));
if ui.button("Increment").clicked() {
count.set_next(*count + 1);
}let count = ui.use_persisted_state(|| 0usize, ());
ui.label(format!("Count: {}", count));
if ui.button("Increment").clicked() {
count.set_next(*count + 1);
}let count = ui.use_state(|| 0usize, ());
let memo = ui.use_memo(
|| {
println!("Calculating memoized value");
count.pow(2)
},
count.clone(),
);
ui.label(format!("Memo: {}", memo));
if ui.button("Increment").clicked() {
count.set_next(*count + 1);
}In the following example, the use_hook_as is almost equivalent to call ui.use_state(|| true, ()) in the show closure but allows you to pass the open state to the Window::open method.
for window in ["window1", "window2", "window3"] {
let mut open = ui
.use_hook_as(egui::Id::new(window), StateHook::new(|| true), ())
.into_var();
egui::Window::new(window)
.open(&mut open)
.show(ui.ctx(), |ui| {
// ...
});
}let count = ui.use_state(|| 0usize, ());
ui.use_effect(|| println!("Count changed to {}", *count), count.clone());ui.use_cleanup(|| println!("This widget is no longer displayed"), ());You can create your own hooks by the two ways.
- Creating a function for a hook
This is the simplest and recommended way to create a custom hook.
fn use_search(ui: &mut Ui, client: &Client) -> Option<SearchResults> {
let text = ui.use_state(|| String::default(), ()).into_var();
ui.text_edit_singleline(&mut *text);
ui.use_effect(|| client.search(&*text), text.clone())
}Or, you can make it to an extension to egui::Ui to make it callable as ui.use_search(client).
trait UseSearchExt {
fn use_search(&mut self, client: &Client) -> Option<SearchResults>;
}
impl UseSearchExt for egui::Ui {
fn use_search(&mut self, client: &Client) -> Option<SearchResults> {
// same as the previous example
}
}- Implement
Hooktrait
All built-in hooks are implemented in this way. This allow you to create a hook with full control, but it is a bit verbose.
impl<D> Hook<D> for MyHook {
type Backend = ()
type Output = usize;
fn init(
&mut self,
_index: usize,
_deps: &D,
_backend: Option<Self::Backend>,
_ui: &mut egui::Ui,
) -> Self::Backend {
}
fn hook(self, backend: &mut Self::Backend, ui: &mut egui::Ui) -> Self::Output {
let count = ui.use_state(0usize, ());
ui.label(format!("Count: {}", count));
if ui.button("Increment").clicked() {
count.set_next(*count + 1);
}
count
}
}