Skip to content

Commit 4a09356

Browse files
committed
Custom storage providers
1 parent fa78d25 commit 4a09356

File tree

10 files changed

+220
-31
lines changed

10 files changed

+220
-31
lines changed

Cargo.lock

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,19 @@ dependencies = [
11281128
"env_logger",
11291129
]
11301130

1131+
[[package]]
1132+
name = "custom_storage"
1133+
version = "0.1.0"
1134+
dependencies = [
1135+
"eframe",
1136+
"egui_demo_lib",
1137+
"egui_extras",
1138+
"env_logger",
1139+
"image",
1140+
"serde",
1141+
"serde_json",
1142+
]
1143+
11311144
[[package]]
11321145
name = "custom_style"
11331146
version = "0.1.0"

crates/eframe/src/epi.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,21 @@ pub enum HardwareAcceleration {
290290
Off,
291291
}
292292

293+
/// Specifies how the app will create a storage provider
294+
#[derive(Default, Clone)]
295+
pub enum StorageProvider {
296+
#[default]
297+
/// eframe will use a default
298+
/// data storage path for each target system.
299+
Default,
300+
/// `eframe` will store the app state in the specified file in the ron format.
301+
/// On web builds, this will behave the same as [`Self::Default`].
302+
AtPath(std::path::PathBuf),
303+
/// Custom storage provider.
304+
/// It allows specifying function that will be called during context creation to provide
305+
Custom(fn(&str) -> Option<Box<dyn Storage>>),
306+
}
307+
293308
/// Options controlling the behavior of a native window.
294309
///
295310
/// Additional windows can be opened using (egui viewports)[`egui::viewport`].
@@ -402,9 +417,9 @@ pub struct NativeOptions {
402417
/// persisted (only if the "persistence" feature is enabled).
403418
pub persist_window: bool,
404419

405-
/// The folder where `eframe` will store the app state. If not set, eframe will use a default
406-
/// data storage path for each target system.
407-
pub persistence_path: Option<std::path::PathBuf>,
420+
/// Specified how the `eframe` would attempt to create a persistent storage where `eframe` will store the app state.
421+
/// If not set, eframe will use a default data storage path for each target system.
422+
pub storage_build: StorageProvider,
408423

409424
/// Controls whether to apply dithering to minimize banding artifacts.
410425
///
@@ -441,7 +456,7 @@ impl Clone for NativeOptions {
441456
#[cfg(feature = "wgpu_no_default_features")]
442457
wgpu_options: self.wgpu_options.clone(),
443458

444-
persistence_path: self.persistence_path.clone(),
459+
storage_build: self.storage_build.clone(),
445460

446461
#[cfg(target_os = "android")]
447462
android_app: self.android_app.clone(),
@@ -484,7 +499,7 @@ impl Default for NativeOptions {
484499

485500
persist_window: true,
486501

487-
persistence_path: None,
502+
storage_build: StorageProviderBuild::Default,
488503

489504
dithering: true,
490505

@@ -545,6 +560,10 @@ pub struct WebOptions {
545560
/// Maximum rate at which to repaint. This can be used to artificially reduce the repaint rate below
546561
/// vsync in order to save resources.
547562
pub max_fps: Option<u32>,
563+
564+
/// Specified how the `eframe` would attempt to create a persistent storage where `eframe` will store the app state.
565+
/// If not set, eframe will use a default data storage path for each target system.
566+
pub storage_build: StorageProvider,
548567
}
549568

550569
#[cfg(target_arch = "wasm32")]
@@ -568,6 +587,7 @@ impl Default for WebOptions {
568587
should_prevent_default: Box::new(|_| true),
569588

570589
max_fps: None,
590+
storage_build: StorageProviderBuild::Default,
571591
}
572592
}
573593
}

crates/eframe/src/native/epi_integration.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,19 @@ pub fn create_storage_with_file(_file: impl Into<PathBuf>) -> Option<Box<dyn epi
145145
None
146146
}
147147

148+
#[cfg(not(target_arch = "wasm32"))]
149+
impl epi::StorageProvider {
150+
/// For loading/saving app state and/or egui memory to disk.
151+
/// Not available on wasm32. For web, use `try_create_storage_web`.
152+
pub fn try_create_storage(&self, app_name: &str) -> Option<Box<dyn epi::Storage>> {
153+
match self {
154+
epi::StorageProviderBuild::Default => create_storage(app_name),
155+
epi::StorageProviderBuild::AtPath(path) => create_storage_with_file(path),
156+
epi::StorageProviderBuild::Custom(f) => f(app_name),
157+
}
158+
}
159+
}
160+
148161
// ----------------------------------------------------------------------------
149162

150163
/// Everything needed to make a winit-based integration for [`epi`].

crates/eframe/src/native/glow_integration.rs

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -196,18 +196,13 @@ impl<'app> GlowWinitApp<'app> {
196196
event_loop: &ActiveEventLoop,
197197
) -> Result<&mut GlowWinitRunning<'app>> {
198198
profiling::function_scope!();
199-
200-
let storage = if let Some(file) = &self.native_options.persistence_path {
201-
epi_integration::create_storage_with_file(file)
202-
} else {
203-
epi_integration::create_storage(
204-
self.native_options
205-
.viewport
206-
.app_id
207-
.as_ref()
208-
.unwrap_or(&self.app_name),
209-
)
210-
};
199+
let storage = self.native_options.storage_build.try_create_storage(
200+
self.native_options
201+
.viewport
202+
.app_id
203+
.as_ref()
204+
.unwrap_or(&self.app_name),
205+
);
211206

212207
let egui_ctx = create_egui_context(storage.as_deref());
213208

crates/eframe/src/native/wgpu_integration.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -406,17 +406,13 @@ impl WinitApp for WgpuWinitApp<'_> {
406406
self.recreate_window(event_loop, running);
407407
running
408408
} else {
409-
let storage = if let Some(file) = &self.native_options.persistence_path {
410-
epi_integration::create_storage_with_file(file)
411-
} else {
412-
epi_integration::create_storage(
413-
self.native_options
414-
.viewport
415-
.app_id
416-
.as_ref()
417-
.unwrap_or(&self.app_name),
418-
)
419-
};
409+
let storage = self.native_options.storage_build.try_create_storage(
410+
self.native_options
411+
.viewport
412+
.app_id
413+
.as_ref()
414+
.unwrap_or(&self.app_name),
415+
);
420416
let egui_ctx = winit_integration::create_egui_context(storage.as_deref());
421417
let (window, builder) = create_window(
422418
&egui_ctx,

crates/eframe/src/web/app_runner.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ impl AppRunner {
9090
},
9191
cpu_usage: None,
9292
};
93-
let storage = LocalStorage::default();
93+
let storage = web_options.storage_build.try_create_storage_web("app_name");
9494

9595
egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent(
9696
&super::user_agent().unwrap_or_default(),
@@ -116,7 +116,7 @@ impl AppRunner {
116116
let cc = epi::CreationContext {
117117
egui_ctx: egui_ctx.clone(),
118118
integration_info: info.clone(),
119-
storage: Some(&storage),
119+
storage: storage.as_deref(),
120120

121121
#[cfg(feature = "glow")]
122122
gl: gl.clone(),
@@ -131,7 +131,7 @@ impl AppRunner {
131131

132132
let frame = epi::Frame {
133133
info,
134-
storage: Some(Box::new(storage)),
134+
storage: storage,
135135

136136
#[cfg(feature = "glow")]
137137
gl,
@@ -425,3 +425,15 @@ impl epi::Storage for LocalStorage {
425425

426426
fn flush(&mut self) {}
427427
}
428+
429+
impl epi::StorageProvider {
430+
/// For loading/saving app state and/or egui memory to disk.
431+
pub fn try_create_storage_web(&self, app_name: &str) -> Option<Box<dyn epi::Storage>> {
432+
match self {
433+
epi::StorageProviderBuild::Default | epi::StorageProviderBuild::AtPath(_) => {
434+
Some(Box::new(LocalStorage::default()))
435+
}
436+
epi::StorageProviderBuild::Custom(f) => f(app_name),
437+
}
438+
}
439+
}

examples/custom_storage/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
custom_storage.json

examples/custom_storage/Cargo.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "custom_storage"
3+
version = "0.1.0"
4+
license = "MIT OR Apache-2.0"
5+
edition = "2024"
6+
rust-version = "1.92"
7+
publish = false
8+
9+
[lints]
10+
workspace = true
11+
12+
13+
[package.metadata.cargo-machete]
14+
ignored = ["image"] # We need the .png feature
15+
16+
17+
[dependencies]
18+
serde = { workspace = true, features = ["derive"] }
19+
serde_json = "1.0"
20+
eframe = { workspace = true, features = [
21+
"default",
22+
"persistence",
23+
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
24+
] }
25+
env_logger = { workspace = true, features = ["auto-color", "humantime"] }
26+
egui_demo_lib.workspace = true
27+
egui_extras = { workspace = true, features = ["image"] }
28+
image = { workspace = true, features = ["png"] }

examples/custom_storage/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Example of how to implement custom storage and use it in application.
2+
3+
```sh
4+
cargo run -p custom_storage
5+
```
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
2+
#![expect(rustdoc::missing_crate_level_docs)] // it's an example
3+
4+
use std::path::PathBuf;
5+
6+
use eframe::{
7+
Storage, StorageProviderBuild,
8+
egui::{self, ahash::HashMap},
9+
};
10+
11+
fn main() -> eframe::Result {
12+
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
13+
let options = eframe::NativeOptions {
14+
viewport: egui::ViewportBuilder::default().with_inner_size([350.0, 590.0]),
15+
storage_build: StorageProviderBuild::Custom(custom_storage),
16+
..Default::default()
17+
};
18+
eframe::run_native(
19+
"egui example: custom style",
20+
options,
21+
Box::new(|cc| Ok(Box::new(MyApp::new(cc)))),
22+
)
23+
}
24+
25+
#[derive(Clone, serde::Serialize, serde::Deserialize)]
26+
struct MyApp {
27+
pub custom_data: String,
28+
pub custom_data2: String,
29+
}
30+
31+
impl Default for MyApp {
32+
fn default() -> Self {
33+
Self {
34+
custom_data: "Hello".to_string(),
35+
custom_data2: "World".to_string(),
36+
}
37+
}
38+
}
39+
40+
impl MyApp {
41+
fn new(cc: &eframe::CreationContext<'_>) -> Self {
42+
egui_extras::install_image_loaders(&cc.egui_ctx); // Needed for the "Widget Gallery" demo
43+
cc.storage
44+
.and_then(|storage| eframe::get_value(storage, "app"))
45+
.unwrap_or_default()
46+
}
47+
}
48+
49+
impl eframe::App for MyApp {
50+
fn save(&mut self, storage: &mut dyn Storage) {
51+
eframe::set_value(storage, "app", &self);
52+
}
53+
54+
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
55+
egui::CentralPanel::default().show_inside(ui, |ui| {
56+
ui.heading("egui using a custom storage for app");
57+
ui.label("Change data and restart the app to see it.");
58+
ui.separator();
59+
ui.text_edit_singleline(&mut self.custom_data);
60+
ui.text_edit_singleline(&mut self.custom_data2);
61+
});
62+
}
63+
}
64+
65+
fn custom_storage(_app_name: &str) -> Option<Box<dyn Storage>> {
66+
CustomStorageData::new(
67+
std::env::current_dir()
68+
.unwrap_or_default()
69+
.join("custom_storage.json"),
70+
)
71+
.map(|data| Box::new(data) as Box<dyn Storage>)
72+
}
73+
74+
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
75+
pub struct CustomStorageData {
76+
hashmap: HashMap<String, String>,
77+
path: PathBuf,
78+
}
79+
80+
impl CustomStorageData {
81+
pub fn new(path: PathBuf) -> Option<Self> {
82+
let hashmap: HashMap<String, String> = std::fs::read(&path)
83+
.ok()
84+
.and_then(|contents| serde_json::from_slice(contents.as_slice()).ok())
85+
.unwrap_or_default();
86+
87+
Some(Self { hashmap, path })
88+
}
89+
}
90+
91+
impl Storage for CustomStorageData {
92+
fn get_string(&self, key: &str) -> Option<String> {
93+
self.hashmap.get(key).cloned()
94+
}
95+
96+
fn set_string(&mut self, key: &str, value: String) {
97+
self.hashmap.insert(key.to_string(), value);
98+
}
99+
100+
fn flush(&mut self) {
101+
let Ok(content) = serde_json::to_string_pretty(&self.hashmap) else {
102+
return;
103+
};
104+
_ = std::fs::write(&self.path, content);
105+
}
106+
}

0 commit comments

Comments
 (0)