Dynamic color palettes without restart (#594)

* dash-frontend: dynamic color palette switching

* wayvr: add support for color dynamic color palette refresh
This commit is contained in:
oo8dev 2026-07-25 18:40:07 +02:00 committed by GitHub
parent 9b336535f7
commit e40b953e5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 95 additions and 104 deletions

View File

@ -23,7 +23,7 @@
<div gap="16" align_items="center">
<!-- Back button -->
<Button id="but_back" width="48" height="48" color="on_background_contrast(transparent)" border_color="on_background_contrast(transparent)">
<sprite src_builtin="dashboard/back.svg" width="24" height="24" />
<sprite src_builtin="dashboard/back.svg" width="24" height="24" color="on_background_contrast" />
</Button>
<!-- Title -->

View File

@ -191,7 +191,6 @@
"COLOR_PALETTES": "Farbpaletten",
"COLOR_PALETTE_CURRENT": "Derzeit aktives Thema.",
"COLOR_PALETTE_ACTIVATE": "Klicken Sie hier, um dieses Thema zu verwenden.",
"APPLY_CHANGES_RESTART": "WayVR neu starten, um die Änderungen zu übernehmen?",
"COLOR_PALETTE_HELP": "Klicken, um Farbpaletten zu durchsuchen.\nUm eigene zu erstellen, besuche die Webseite!",
"COLOR_PALETTE_CUSTOM_URL": "Füge dein eigenes benutzerdefiniertes Thema hinzu!",
"COLOR_PALETTE_CUSTOM_URL2": "Klicken Sie hier, um das Web-Handbuch zu öffnen.",
@ -329,4 +328,4 @@
"REMOVE_FROM_FAVOURITES": "Aus Favoriten entfernen",
"EMPTY": "Leer",
"PINNED_APPS": "Angepinnte Anwendungen"
}
}

View File

@ -103,7 +103,6 @@
"COLOR_PALETTE_CUSTOM_URL": "Add your own custom theme!",
"COLOR_PALETTE_CUSTOM_URL2": "Click to open web manual.",
"COLOR_PALETTE_ACTIVATE": "Click to use this theme.",
"APPLY_CHANGES_RESTART": "Restart WayVR to apply changes?",
"CLEAR_PIPEWIRE_TOKENS": "Clear PipeWire tokens",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Prompt for screen selection on next start",
"CLEAR_SAVED_STATE": "Clear saved state",

View File

@ -191,7 +191,6 @@
"COLOR_PALETTES": "Paletas de colores",
"COLOR_PALETTE_CURRENT": "Tema actualmente activo.",
"COLOR_PALETTE_ACTIVATE": "Haz clic para usar este tema.",
"APPLY_CHANGES_RESTART": "¿Reiniciar WayVR para aplicar los cambios?",
"COLOR_PALETTE_HELP": "Haz clic para explorar paletas de colores.\nPara crear paletas personalizadas, ¡visita el sitio web!",
"COLOR_PALETTE_CUSTOM_URL": "¡Añade tu propio tema personalizado!",
"COLOR_PALETTE_CUSTOM_URL2": "Haz clic para abrir el manual web.",
@ -329,4 +328,4 @@
"REMOVE_FROM_FAVOURITES": "Eliminar de favoritos",
"EMPTY": "Vacío",
"PINNED_APPS": "Aplicaciones ancladas"
}
}

View File

@ -212,7 +212,6 @@
"COLOR_PALETTES": "Tavolozze colori",
"COLOR_PALETTE_CURRENT": "Tema attualmente attivo.",
"COLOR_PALETTE_ACTIVATE": "Clicca per usare questo tema.",
"APPLY_CHANGES_RESTART": "Riavviare WayVR per applicare le modifiche?",
"COLOR_PALETTE_HELP": "Clicca per sfogliare le palette di colori.\nPer crearne di personalizzate, visita il sito web!",
"COLOR_PALETTE_CUSTOM_URL": "Aggiungi il tuo tema personalizzato!",
"COLOR_PALETTE_CUSTOM_URL2": "Clicca per aprire il manuale web.",
@ -329,4 +328,4 @@
"REMOVE_FROM_FAVOURITES": "Rimuovi dai preferiti",
"EMPTY": "Vuoto",
"PINNED_APPS": "Applicazioni fissate"
}
}

View File

@ -191,7 +191,6 @@
"COLOR_PALETTES": "カラーパレット",
"COLOR_PALETTE_CURRENT": "現在有効なテーマ。",
"COLOR_PALETTE_ACTIVATE": "クリックしてこのテーマを適用します。",
"APPLY_CHANGES_RESTART": "変更を適用するためにWayVRを再起動しますか",
"COLOR_PALETTE_HELP": "クリックしてカラーパレットを選択してください。\nカスタムパレットの作成については、ウェブサイトを参照してください",
"COLOR_PALETTE_CUSTOM_URL": "カスタムテーマを追加しましょう!",
"COLOR_PALETTE_CUSTOM_URL2": "クリックしてウェブマニュアルを開く。",
@ -329,4 +328,4 @@
"REMOVE_FROM_FAVOURITES": "お気に入りから削除",
"EMPTY": "空",
"PINNED_APPS": "ピン留めされたアプリケーション"
}
}

View File

@ -186,7 +186,6 @@
"COLOR_PALETTES": "Palety kolorów",
"COLOR_PALETTE_CURRENT": "Obecnie aktywny motyw.",
"COLOR_PALETTE_ACTIVATE": "Kliknij, aby użyć tego motywu.",
"APPLY_CHANGES_RESTART": "Zrestartować WayVR, aby zastosować zmiany?",
"COLOR_PALETTE_HELP": "Kliknij, aby przeglądać palety kolorów.\nAby stworzyć własne, odwiedź stronę internetową!",
"COLOR_PALETTE_CUSTOM_URL": "Dodaj własny motyw!",
"COLOR_PALETTE_CUSTOM_URL2": "Kliknij, aby otworzyć instrukcję internetową.",
@ -329,4 +328,4 @@
"REMOVE_FROM_FAVOURITES": "Usuń z ulubionych",
"EMPTY": "Puste",
"PINNED_APPS": "Przypięte aplikacje"
}
}

View File

@ -212,7 +212,6 @@
"COLOR_PALETTES": "配色方案",
"COLOR_PALETTE_CURRENT": "当前激活的主题。",
"COLOR_PALETTE_ACTIVATE": "点击以使用此主题。",
"APPLY_CHANGES_RESTART": "重启 WayVR 以应用更改吗?",
"COLOR_PALETTE_HELP": "点击浏览配色方案。\n如需创建自定义配色请参阅网站",
"COLOR_PALETTE_CUSTOM_URL": "添加自定义主题!",
"COLOR_PALETTE_CUSTOM_URL2": "点击打开网页版手册。",
@ -329,4 +328,4 @@
"REMOVE_FROM_FAVOURITES": "从收藏中移除",
"EMPTY": "空",
"PINNED_APPS": "已固定应用程序"
}
}

View File

@ -11,7 +11,7 @@ use wgui::{
use crate::{
frontend::FrontendTasks,
tab::settings::{
SettingType, SettingsMountParams, SettingsTab, Task as SettingsTask, horiz_cell,
SettingType, SettingsMountParams, SettingsTab, horiz_cell,
macros::{options_category, options_checkbox, options_dropdown, options_slider_f32},
mount_requires_restart,
},
@ -29,7 +29,6 @@ pub struct State {
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
tasks: Tasks<Task>,
settings_tasks: Tasks<SettingsTask>,
}
impl SettingsTab for State {
@ -43,7 +42,6 @@ impl SettingsTab for State {
self.frontend_tasks.clone(),
self.globals.clone(),
self.popup_color_palettes.clone(),
self.settings_tasks.clone(),
par.general_config.color_palette.clone(),
);
}
@ -81,7 +79,6 @@ impl State {
frontend_tasks: par.frontend_tasks.clone(),
globals: par.mp.doc_params.globals.clone(),
tasks,
settings_tasks: par.mp.tasks.clone(),
})
}
}

View File

@ -1,11 +1,16 @@
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::popup_manager::{MountPopupOnceParams, PopupHolder},
views::{ViewTrait, ViewUpdateParams},
};
use std::{rc::Rc, sync::Arc};
use wgui::color::WguiColorName;
use wgui::{
assets::AssetPath,
color::WguiColorName,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
layout::{Layout, LayoutTask, WidgetID},
log::LogErr,
palette::PALETTES,
parser::{Fetchable, ParseDocumentParams, TemplateParams},
@ -16,19 +21,10 @@ use wlx_common::{
palette::{list_palette_files, load_custom_palette},
};
use crate::{
frontend::{FrontendTask, FrontendTasks},
tab::settings::Task as SettingsTask,
util::popup_manager::{MountPopupOnceParams, PopupHolder},
views::{self, ViewTrait, ViewUpdateParams},
};
#[derive(Clone)]
enum Task {
SelectPalette(String),
CustomPaletteUrl,
Restart,
Cancel,
}
pub struct Params<'a> {
@ -36,40 +32,30 @@ pub struct Params<'a> {
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub frontend_tasks: &'a FrontendTasks,
pub settings_tasks: Tasks<SettingsTask>,
pub on_close_request: Box<dyn FnOnce()>,
pub current_palette: Arc<str>,
}
pub struct View {
tasks: Tasks<Task>,
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
popup_dialog: PopupHolder<views::dialog_box::View>,
settings_tasks: Tasks<SettingsTask>,
chosen_palette: Option<Arc<str>>,
on_close_request: Option<Box<dyn FnOnce()>>,
}
impl ViewTrait for View {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
self.popup_dialog.update(par)?;
for task in self.tasks.drain() {
match task {
Task::SelectPalette(profile) => {
self.chosen_palette = Some(profile.into());
self.show_restart_dialog_box()?;
}
Task::Cancel => {
let close_dialog = self.popup_dialog.get_close_callback(par.layout);
close_dialog();
}
Task::Restart => {
if let Some(palette) = self.chosen_palette.take() {
par.general_config.color_palette = palette;
par.config_change_kind.replace(ConfigChangeKind::WguiThemeChange);
let mut globals = par.layout.state.globals.get();
let new_palette = wlx_common::palette::load_palette(&profile);
globals.palette = new_palette;
par.general_config.color_palette = profile.into();
par.layout.tasks.push(LayoutTask::RefreshPalette);
par.config_change_kind.replace(ConfigChangeKind::WguiColorPaletteChange);
if let Some(c) = self.on_close_request.take() {
c();
}
self.settings_tasks.push(SettingsTask::RestartSoftware);
}
Task::CustomPaletteUrl => {
self.frontend_tasks.push(FrontendTask::OpenURL(
@ -113,7 +99,6 @@ impl View {
let list_parent = parser_state.fetch_widget(&params.layout.state, "list_parent")?.id;
let tasks = Tasks::new();
let popup_dialog = PopupHolder::<views::dialog_box::View>::default();
for (idx, name) in list_palette_files().into_iter().enumerate() {
let Ok(palette) = load_custom_palette(&name).log_warn("Could not load custom color palette") else {
@ -246,58 +231,15 @@ impl View {
Ok(Self {
tasks,
frontend_tasks: params.frontend_tasks.clone(),
globals: params.globals.clone(),
popup_dialog,
settings_tasks: params.settings_tasks,
chosen_palette: None,
on_close_request: Some(params.on_close_request),
})
}
fn show_restart_dialog_box(&mut self) -> anyhow::Result<()> {
const ACTION_RESTART: &str = "restart";
const ACTION_CANCEL: &str = "cancel";
let tasks = self.tasks.clone();
views::dialog_box::mount_popup(
self.popup_dialog.clone(),
self.frontend_tasks.clone(),
views::dialog_box::Params {
globals: self.globals.clone(),
message: Translation::from_translation_key("APP_SETTINGS.APPLY_CHANGES_RESTART"),
entries: vec![
views::dialog_box::ButtonEntry {
content: Translation::from_translation_key("APP_SETTINGS.CANCEL"),
icon: "dashboard/close.svg",
action: ACTION_CANCEL,
},
views::dialog_box::ButtonEntry {
content: Translation::from_translation_key("APP_SETTINGS.RESTART_SOFTWARE"),
icon: "dashboard/refresh.svg",
action: ACTION_RESTART,
},
],
on_action_click: Box::new(move |action| match action {
ACTION_RESTART => {
tasks.push(Task::Restart);
}
ACTION_CANCEL => {
tasks.push(Task::Cancel);
}
_ => unreachable!(),
}),
},
);
Ok(())
}
}
pub fn mount_popup(
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
popup: PopupHolder<View>,
settings_tasks: Tasks<SettingsTask>,
current_palette: Arc<str>,
) {
frontend_tasks
@ -305,13 +247,14 @@ pub fn mount_popup(
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_translation_key("APP_SETTINGS.COLOR_PALETTES"),
Box::new(move |data| {
let on_close_request = popup.get_close_callback(data.layout);
let view = View::new(Params {
globals: globals.clone(),
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
settings_tasks,
current_palette,
on_close_request,
})?;
popup.set_view(data.handle, view, None);

View File

@ -107,6 +107,7 @@ pub enum SpawnPos {
pub enum GlobalChange {
Settings,
Keyboard,
ColorPalette,
}
pub type ModifyOverlayTask = dyn FnOnce(&mut AppState, &mut OverlayWindowConfig) + Send;

View File

@ -29,7 +29,7 @@ use wgui::{
},
gfx::cmd::WGfxClearMode,
i18n::Translation,
layout::{Layout, LayoutParams, LayoutUpdateParams, WidgetID},
layout::{Layout, LayoutParams, LayoutTask, LayoutUpdateParams, WidgetID},
parser::{self, CustomAttribsInfoOwned, Fetchable, ParseDocumentExtra, ParserState},
renderer_vk::{context::Context as WguiContext, text::custom_glyph::CustomGlyphData},
widget::{
@ -360,6 +360,9 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
}
fn notify(&mut self, app: &mut AppState, data: OverlayEventData) -> anyhow::Result<()> {
if let OverlayEventData::ColorPaletteRefresh = data {
self.layout.tasks.push(LayoutTask::RefreshPalette);
}
let Some(on_notify) = self.on_notify.take() else {
return Ok(());
};

View File

@ -538,11 +538,17 @@ impl DashInterface<AppState> for DashInterfaceLive {
data.session.config_dirty = true;
match kind {
ConfigChangeKind::WguiColorPaletteChange => {
data.tasks
.enqueue(TaskType::Overlay(OverlayTask::GlobalChange(
GlobalChange::ColorPalette,
)));
}
ConfigChangeKind::OverlayConfig => {
data.tasks
.enqueue(TaskType::Overlay(OverlayTask::GlobalChange(
GlobalChange::Settings,
)))
)));
}
ConfigChangeKind::EnvironmentBlend => {
#[cfg(feature = "openxr")]

View File

@ -189,8 +189,6 @@ impl AppState {
)
.ok();
let palette = load_palette(&*session.config.color_palette);
let mut app_state = Self {
tasks,
gfx,
@ -208,7 +206,7 @@ impl AppState {
&lang_provider,
&WguiFontConfig::default(),
get_config_file_path(&theme_path),
palette,
load_palette(&*session.config.color_palette),
)?,
wgui_theme: Rc::new(theme),
dbus,

View File

@ -130,6 +130,7 @@ pub enum OverlayEventData {
},
WvrCommand(WvrCommand),
ResizeRequest([u32; 2]),
ColorPaletteRefresh,
}
pub trait OverlayBackend: Any {

View File

@ -333,6 +333,20 @@ where
self.overlays_changed(app)?;
self.sets_changed(app);
}
OverlayTask::GlobalChange(GlobalChange::ColorPalette) => {
{
let mut globals = app.wgui_globals.get();
globals.palette =
wlx_common::palette::load_palette(&app.session.config.color_palette);
}
for o in self.overlays.values_mut() {
let _ = o
.config
.backend
.notify(app, OverlayEventData::ColorPaletteRefresh);
}
}
OverlayTask::CleanupMirrors => {
let mut ids_to_remove = vec![];
for (oid, o) in &self.overlays {

View File

@ -155,6 +155,7 @@ pub enum LayoutTask {
PlaySound(WguiSoundType),
Dispatch(LayoutDispatchFunc),
SetFocus(ComponentWeak),
RefreshPalette,
Unfocus,
}
@ -748,6 +749,10 @@ impl Layout {
let mut tasks = self.tasks.drain();
while let Some(task) = tasks.pop_front() {
match task {
LayoutTask::RefreshPalette => {
let root = self.tree_root_widget;
LayoutState::refresh_palette_recur(&mut self.common(), root);
}
LayoutTask::RemoveWidget(widget_id) => {
self.remove_widget(widget_id);
}
@ -1028,4 +1033,27 @@ impl LayoutState {
// safety: we just checked the type
unsafe { Ok(Rc::from_raw(Rc::into_raw(component.0).cast())) }
}
fn refresh_palette_recur(common: &mut CallbackDataCommon, widget_id: WidgetID) {
if let Some(widget) = common.state.widgets.get(widget_id) {
widget.state().obj.palette_updated(common);
} else {
debug_assert!(false);
return;
}
let Some(node_id) = common.state.nodes.get(widget_id) else {
/* node/widget desync, this shouldn't happen */
debug_assert!(false);
return;
};
for child_node_id in common.state.tree.child_ids(*node_id) {
let Some(widget_id) = common.state.tree.get_node_context(child_node_id) else {
debug_assert!(false);
continue;
};
LayoutState::refresh_palette_recur(common, *widget_id);
}
}
}

View File

@ -22,7 +22,7 @@ impl WguiColorPalette {
}
log::warn!("No color palette with name '{palette_name}', using default colors.");
return DEFAULT.clone();
DEFAULT.clone()
}
pub const fn from_inner(colors: [drawing::Color; WguiColorName::COUNT]) -> Self {

View File

@ -129,11 +129,11 @@ impl WidgetLabel {
}
}
pub fn get_color(&self) -> WguiColor {
pub const fn get_color(&self) -> WguiColor {
self.params.style.color.unwrap() // create_ex populates this
}
pub fn parent_color(&self) -> ParentColor {
pub const fn parent_color(&self) -> ParentColor {
self.params.parent_color
}
}
@ -188,6 +188,11 @@ impl WidgetObj for WidgetLabel {
})
}
fn palette_updated(&mut self, common: &mut CallbackDataCommon) {
let cur_color = self.get_color();
self.set_color(common, cur_color, true);
}
fn get_id(&self) -> WidgetID {
self.id
}

View File

@ -191,6 +191,8 @@ pub trait WidgetObj: AnyTrait {
fn draw(&mut self, state: &mut DrawState, params: &DrawParams);
fn palette_updated(&mut self, _common: &mut CallbackDataCommon) {}
fn measure(
&mut self,
_globals: &Globals,

View File

@ -84,7 +84,7 @@ pub trait DashInterface<T> {
pub enum ConfigChangeKind {
OverlayConfig,
EnvironmentBlend,
WguiThemeChange, // TODO: does not do anything right now
WguiColorPaletteChange,
WvrServerConfig,
/// Marks the config for saving but doesn't notify any components
#[default]