Avoid cold theme preview rasterization and load settings schema asynchronously

This commit is contained in:
aria committed 2026-10-28 06:28:06 +09:00
1 parent 326465952a
commit 6671960d5e
6 files changed
+514 -35

No files matched your search

+102 -14
View File
@@ -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(),
+39 -3
View File
@@ -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, &current);
ferese_config::locale::set(ferese_config::locale::from_document(&current.doc));
let options = all_options::State::load(&current);
let options = all_options::State::pending(&current);
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();