Avoid cold theme preview rasterization and load settings schema asynchronously
This commit is contained in:
1 parent
326465952a
commit
6671960d5e
6 files changed
+514
-35
No files matched your search
@@ -7,6 +7,7 @@ use super::{
|
||||
use ferese_config::locale::{text as t, translate as tr};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub const GROUPS: &[(&str, &[&str])] = &[
|
||||
("General", &["ui", "status"]),
|
||||
@@ -64,7 +65,9 @@ pub enum Input {
|
||||
pub struct State {
|
||||
pub filter: String,
|
||||
pub applying: bool,
|
||||
schema: Value,
|
||||
schema: Arc<Value>,
|
||||
ready: bool,
|
||||
loading: bool,
|
||||
baseline: Value,
|
||||
value: Value,
|
||||
base_source: String,
|
||||
@@ -74,31 +77,44 @@ pub struct State {
|
||||
error: Option<String>,
|
||||
}
|
||||
impl State {
|
||||
pub fn load(snapshot: &Snapshot) -> Self {
|
||||
static CATALOG: std::sync::OnceLock<Result<Value, String>> = std::sync::OnceLock::new();
|
||||
let result = CATALOG.get_or_init(load_schema).clone();
|
||||
let (schema, error) = match result {
|
||||
Ok(v) => (v, None),
|
||||
Err(e) => (
|
||||
Value::Null,
|
||||
Some(format!("{}: {e}", t("Cannot load configuration schema"))),
|
||||
),
|
||||
};
|
||||
pub fn pending(snapshot: &Snapshot) -> Self {
|
||||
let mut state = Self {
|
||||
filter: String::new(),
|
||||
applying: false,
|
||||
schema,
|
||||
schema: Arc::new(Value::Null),
|
||||
ready: false,
|
||||
loading: false,
|
||||
baseline: Value::Null,
|
||||
value: Value::Null,
|
||||
base_source: String::new(),
|
||||
expanded: HashSet::new(),
|
||||
text: HashMap::new(),
|
||||
keys: HashMap::new(),
|
||||
error,
|
||||
error: None,
|
||||
};
|
||||
state.reset(snapshot);
|
||||
state
|
||||
}
|
||||
|
||||
fn loaded(&mut self, result: Result<Arc<Value>, String>, snapshot: &Snapshot) {
|
||||
self.loading = false;
|
||||
match result {
|
||||
Ok(schema) => {
|
||||
self.schema = schema;
|
||||
self.ready = true;
|
||||
self.error = None;
|
||||
// Use the current document, which may have changed while the
|
||||
// schema worker was running. Keep the user's search filter.
|
||||
self.reset(snapshot);
|
||||
}
|
||||
Err(error) => {
|
||||
self.error = Some(format!(
|
||||
"{}: {error}",
|
||||
t("Cannot load configuration schema")
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn reset(&mut self, snapshot: &Snapshot) {
|
||||
self.applying = false;
|
||||
self.baseline = self.schema["defaults"].clone();
|
||||
@@ -337,6 +353,17 @@ fn load_schema() -> Result<Value, String> {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
fn catalog() -> Result<Arc<Value>, String> {
|
||||
static CATALOG: std::sync::OnceLock<Arc<Value>> = std::sync::OnceLock::new();
|
||||
if let Some(catalog) = CATALOG.get() {
|
||||
return Ok(catalog.clone());
|
||||
}
|
||||
let schema = Arc::new(load_schema()?);
|
||||
// Cache successful results only, so a missing backend can be retried.
|
||||
let _ = CATALOG.set(schema.clone());
|
||||
Ok(schema)
|
||||
}
|
||||
fn overlay(base: &mut Value, addition: &Value) {
|
||||
if let (Some(a), Some(b)) = (base.as_object_mut(), addition.as_object()) {
|
||||
for (k, v) in b {
|
||||
@@ -410,6 +437,25 @@ fn label(k: &str) -> String {
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(super) fn load_options(&mut self) -> Task<Message> {
|
||||
if self.options.ready || self.options.loading {
|
||||
return Task::none();
|
||||
}
|
||||
self.options.loading = true;
|
||||
self.options.error = None;
|
||||
cosmic::task::future(async {
|
||||
Message::OptionsLoaded(
|
||||
tokio::task::spawn_blocking(catalog)
|
||||
.await
|
||||
.unwrap_or_else(|error| Err(error.to_string())),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn options_loaded(&mut self, result: Result<Arc<Value>, String>) {
|
||||
self.options.loaded(result, &self.current);
|
||||
}
|
||||
|
||||
pub fn options_input(&mut self, input: Input) -> Task<Message> {
|
||||
let state = &mut self.options;
|
||||
match input {
|
||||
@@ -581,6 +627,25 @@ impl App {
|
||||
.into()
|
||||
}
|
||||
pub fn all_options_view(&self) -> Element<'_, Message> {
|
||||
if !self.options.ready {
|
||||
let mut content = column([]).spacing(10).push(
|
||||
self.note(
|
||||
self.options
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or(t("Loading configuration options…")),
|
||||
),
|
||||
);
|
||||
if !self.options.loading {
|
||||
content = content.push(self.settings_button(
|
||||
t("Retry"),
|
||||
"M4 10a8 8 0 1 1 0 4 M4 4v6h6",
|
||||
Some(Message::LoadOptions),
|
||||
false,
|
||||
));
|
||||
}
|
||||
return content.into();
|
||||
}
|
||||
let mut out = column([]).spacing(10).push(self.note(t(
|
||||
"Advanced changes are saved together. Press Enter to confirm text fields, then Apply.",
|
||||
)));
|
||||
@@ -1033,11 +1098,34 @@ fn variant_label(s: &Value) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn delayed_schema_uses_latest_configuration_and_preserves_search() {
|
||||
let original = Snapshot::parse("input { repeat-rate 25; }".into()).unwrap();
|
||||
let latest = Snapshot::parse("input { repeat-rate 80; }".into()).unwrap();
|
||||
let mut pending = State::pending(&original);
|
||||
assert!(!pending.ready);
|
||||
pending.filter = "keyboard".into();
|
||||
pending.loading = true;
|
||||
pending.loaded(
|
||||
Ok(Arc::new(json!({"defaults":{"input":{"repeat_rate":30}}}))),
|
||||
&latest,
|
||||
);
|
||||
assert_eq!(pending.value["input"]["repeat_rate"], 80);
|
||||
assert_eq!(pending.filter, "keyboard");
|
||||
assert!(pending.ready);
|
||||
assert!(!pending.loading);
|
||||
assert!(!pending.dirty());
|
||||
}
|
||||
|
||||
fn state(snapshot: &Snapshot) -> State {
|
||||
let mut state = State {
|
||||
filter: String::new(),
|
||||
applying: false,
|
||||
schema: json!({"defaults":{"ui":{"language":"auto"},"input":{"repeat_rate":25,"xkb_layout":"us"},"commands":{}}}),
|
||||
schema: Arc::new(
|
||||
json!({"defaults":{"ui":{"language":"auto"},"input":{"repeat_rate":25,"xkb_layout":"us"},"commands":{}}}),
|
||||
),
|
||||
ready: true,
|
||||
loading: false,
|
||||
baseline: Value::Null,
|
||||
value: Value::Null,
|
||||
base_source: String::new(),
|
||||
|
||||
@@ -114,6 +114,8 @@ enum InitialPage {
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
Options(all_options::Input),
|
||||
LoadOptions,
|
||||
OptionsLoaded(Result<std::sync::Arc<serde_json::Value>, String>),
|
||||
TrayApps(Vec<tray_controls::Application>),
|
||||
RefreshTrayApps,
|
||||
ReadRenderScale(cosmic::iced::window::Id),
|
||||
@@ -289,7 +291,7 @@ impl cosmic::Application for App {
|
||||
let native_palette = visuals::Palette::from_resolved(&resolved.presented);
|
||||
let family_sections = theme_controls::FamilySections::new(&resolved.families, ¤t);
|
||||
ferese_config::locale::set(ferese_config::locale::from_document(¤t.doc));
|
||||
let options = all_options::State::load(¤t);
|
||||
let options = all_options::State::pending(¤t);
|
||||
let mut app = Self {
|
||||
tray_apps: Vec::new(),
|
||||
options,
|
||||
@@ -380,7 +382,15 @@ impl cosmic::Application for App {
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
(app, Task::batch([task, connections, thumbnails, displays]))
|
||||
let options = if app.page == Page::Configuration {
|
||||
app.load_options()
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
(
|
||||
app,
|
||||
Task::batch([task, connections, thumbnails, displays, options]),
|
||||
)
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
@@ -388,6 +398,8 @@ impl cosmic::Application for App {
|
||||
Message::TrayApps(apps) => self.tray_apps = apps,
|
||||
Message::RefreshTrayApps => return self.load_tray_apps(),
|
||||
Message::Options(input) => return self.options_input(input),
|
||||
Message::LoadOptions => return self.load_options(),
|
||||
Message::OptionsLoaded(result) => self.options_loaded(result),
|
||||
Message::DragWindow => return self.core.drag(None),
|
||||
Message::ToggleMaximize => return self.core.toggle_maximize(None),
|
||||
Message::ReadRenderScale(id) => {
|
||||
@@ -580,6 +592,7 @@ impl cosmic::Application for App {
|
||||
Page::Displays | Page::Bar => self.load_displays(),
|
||||
Page::Wallpaper => self.load_thumbnail(),
|
||||
Page::Tray => self.load_tray_apps(),
|
||||
Page::Configuration => self.load_options(),
|
||||
_ => Task::none(),
|
||||
};
|
||||
return Task::batch([reset, cleanup, load]);
|
||||
@@ -596,6 +609,9 @@ impl cosmic::Application for App {
|
||||
if page == Page::Wallpaper {
|
||||
return Task::batch([reset, self.load_thumbnail()]);
|
||||
}
|
||||
if page == Page::Configuration {
|
||||
return Task::batch([reset, self.load_options()]);
|
||||
}
|
||||
|
||||
return reset;
|
||||
}
|
||||
@@ -643,7 +659,12 @@ impl cosmic::Application for App {
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Search(query) => self.search = query,
|
||||
Message::Search(query) => {
|
||||
self.search = query;
|
||||
if !self.search.is_empty() {
|
||||
return self.load_options();
|
||||
}
|
||||
}
|
||||
Message::Draft(path, value) => {
|
||||
self.inputs.insert(path, value);
|
||||
}
|
||||
@@ -1326,6 +1347,21 @@ mod tests {
|
||||
.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_loading_is_lazy_deduplicated_and_retries_after_failure() {
|
||||
let mut app = app();
|
||||
assert_eq!(app.page, Page::Appearance);
|
||||
assert_eq!(app.load_options().units(), 1);
|
||||
assert_eq!(app.load_options().units(), 0);
|
||||
app.options_loaded(Err("temporary backend failure".into()));
|
||||
assert_eq!(app.load_options().units(), 1);
|
||||
app.options_loaded(Ok(std::sync::Arc::new(serde_json::json!({
|
||||
"schema": {"properties": {}}, "defaults": {}
|
||||
}))));
|
||||
assert_eq!(app.load_options().units(), 0);
|
||||
assert!(!app.options.dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_drag_targets_the_main_window_and_handles_a_missing_window() {
|
||||
let mut app = app();
|
||||
|
||||
Reference in new issue
Block a user