| Crates.io | egui_hooks |
| lib.rs | egui_hooks |
| version | 0.8.0 |
| created_at | 2024-01-04 15:31:02.112665+00 |
| updated_at | 2025-03-29 13:58:45.026886+00 |
| description | React Hooks like API for egui |
| homepage | |
| repository | https://github.com/ryo33/egui_hooks |
| max_upload_size | |
| id | 1088743 |
| size | 173,576 |
React Hooks like API for enhancing the ergonomics of egui::Memory
| egui version | egui_hooks version |
|---|---|
| egui@0.24.0 | 0.1.2 |
| egui@0.25.0 | 0.2.0 |
| egui@0.26.0 | 0.3.0 |
| egui@0.27.0 | 0.4.0 |
| egui@0.28.0 | 0.5.0 |
| egui@0.29.0 | 0.6.0 |
| egui@0.30.0 | 0.7.0 |
| egui@0.31.0 | 0.8.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.
egui::Memory directly, the states are
automatically freed from the HashMap when the widget will be no longer
displayed. This is based on TwoFrameMap (2f kv) defined in this crate.ui.data(|| { ... }).
This is because hooks encapsulate the underlying RwLock operation.use_state(|| user_id.clone(), user_id) or
use_effect(|| log(input), input), so you can precisely track the
dependencies without manually writing if statements on state change.egui::Memory is 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:
use_state, it creates a Arc<ArcSwap<usize>> in the
egui::Memory with the default value.dep is changed since the last frame, it stores the default value to
the existing ArcSwap.Var<usize> to the caller.Deref or DerefMut the Var in their widget code.Var is dropped, it stores the updated value to the ArcSwap.ArcSwap is removed from the
egui::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_state for states in a specific widget (e.g. animation state, scroll
position)use_state with into_var() to feed a variable in-place to Window::open or
TextEdit::singlelineuse_memo, use_cache for caching expensive calculationuse_effect, use_future for side effects (e.g. logging, network request)use_global for global settings (e.g. theme, locale)use_kv for sharing states between widgets (e.g. getting a position of a
specific widget)use_ephemeral_kv for storing events in the current frame (e.g. providing
custom response on a custom widget)use_previous_measurement for using the previous frame result for layoutinguse_measurement for calculating and memoizing the size of a widget for
layoutinguse_memouse_effectuse_effect_with_cleanupuse_state, use_persisted_statestate.into_var() to use state as a variableuse_kv, use_persisted_kvuse_2f_kv, use_persisted_2f_kvuse_ephemeral_kvuse_global, use_persisted_global, and use_ephemeral_globaluse_cache (a thin wrapper of caches in egui::Memory)use_previous_measurementuse_measurement (calculate the size of the widget without fear of the
2^N problem.use_future (needs tokio feature)use_throttle and use_debounceuse_drag_originuse_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.
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
}
}
Hook traitAll 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
}
}