custom palettes

This commit is contained in:
galister 2026-07-14 00:00:06 +09:00
parent c74c51f7cd
commit fcc84e56b3
18 changed files with 306 additions and 31 deletions

View File

@ -1,5 +1,4 @@
<layout>
<!-- id, text -->
<template name="ColorPaletteButton">
<Button id="${id}" width="350" flex_direction="column" justify_content="space_around" tooltip="${tooltip}" tooltip_side="bottom"
color="${background}" hover_color="${background_variant}" border_color="${outline}" hover_border_color="${highlight}" padding="16" margin="4" >
@ -13,6 +12,13 @@
</Button>
</template>
<template name="CustomPaletteButton">
<Button id="custom_btn" width="350" flex_direction="column" justify_content="space_around" padding="16" margin="4" >
<label translation="APP_SETTINGS.COLOR_PALETTE_CUSTOM_URL" size="16" weight="bold" />
<label translation="APP_SETTINGS.COLOR_PALETTE_CUSTOM_URL2" size="14" color="on_background_variant" />
</Button>
</template>
<elements>
<div flex_direction="column" gap="8" width="100%" align_items="center">
<div id="list_parent" flex_grow="1" flex_wrap="wrap" flex_direction="row" gap="4" width="100%" justify_content="center"></div>

View File

@ -190,7 +190,9 @@
"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_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."
},
"HELLO": "Hallo!",
"AUDIO": {

View File

@ -100,6 +100,8 @@
"COLOR_PALETTE_HELP": "Click to browse color palettes.\nTo create custom ones, see website!",
"COLOR_PALETTES": "Color palettes",
"COLOR_PALETTE_CURRENT": "Currently active theme.",
"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",

View File

@ -190,7 +190,9 @@
"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_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."
},
"HELLO": "¡Hola!",
"AUDIO": {

View File

@ -211,7 +211,9 @@
"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_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."
},
"APPLICATION_LAUNCHER": "Lanciatore applicazioni",
"APPLICATION_STARTED": "Applicazione avviata",

View File

@ -190,7 +190,9 @@
"COLOR_PALETTE_CURRENT": "現在有効なテーマ。",
"COLOR_PALETTE_ACTIVATE": "クリックしてこのテーマを適用します。",
"APPLY_CHANGES_RESTART": "変更を適用するためにWayVRを再起動しますか",
"COLOR_PALETTE_HELP": "クリックしてカラーパレットを選択してください。\nカスタムパレットの作成については、ウェブサイトを参照してください"
"COLOR_PALETTE_HELP": "クリックしてカラーパレットを選択してください。\nカスタムパレットの作成については、ウェブサイトを参照してください",
"COLOR_PALETTE_CUSTOM_URL": "カスタムテーマを追加しましょう!",
"COLOR_PALETTE_CUSTOM_URL2": "クリックしてウェブマニュアルを開く。"
},
"HELLO": "こんにちは!",
"AUDIO": {

View File

@ -185,7 +185,9 @@
"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_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ą."
},
"APPLICATION_LAUNCHER": "Uruchamiacz aplikacji",
"APPLICATIONS": "Aplikacje",

View File

@ -211,7 +211,9 @@
"COLOR_PALETTE_CURRENT": "当前激活的主题。",
"COLOR_PALETTE_ACTIVATE": "点击以使用此主题。",
"APPLY_CHANGES_RESTART": "重启 WayVR 以应用更改吗?",
"COLOR_PALETTE_HELP": "点击浏览配色方案。\n如需创建自定义配色请参阅网站"
"COLOR_PALETTE_HELP": "点击浏览配色方案。\n如需创建自定义配色请参阅网站",
"COLOR_PALETTE_CUSTOM_URL": "添加自定义主题!",
"COLOR_PALETTE_CUSTOM_URL2": "点击打开网页版手册。"
},
"APPLICATION_LAUNCHER": "应用启动器",
"APPLICATION_STARTED": "应用已启动",

View File

@ -24,14 +24,15 @@ use wlx_common::{
audio,
dash_interface::{BoxDashInterface, ConfigChangeKind, RecenterMode},
locale::WayVRLangProvider,
palette::load_palette,
timestep::{self, Timestep},
};
use crate::{
assets,
tab::{
Tab, TabType, apps::TabApps, donate::TabDonate, games::TabGames, home::TabHome, monado::TabMonado,
settings::TabSettings, welcome::TabWelcome,
apps::TabApps, donate::TabDonate, games::TabGames, home::TabHome, monado::TabMonado, settings::TabSettings,
welcome::TabWelcome, Tab, TabType,
},
util::{
popup_manager::{MountPopupOnceParams, PopupManager, PopupManagerParams},
@ -131,6 +132,8 @@ impl<T: 'static> Frontend<T> {
let font_binary_regular = assets.load_from_path_gzip("Quicksand-Regular.ttf.gz")?;
let font_binary_light = assets.load_from_path_gzip("Quicksand-Light.ttf.gz")?;
let palette = load_palette(params.color_palette);
let globals = WguiGlobals::new(
assets,
params.lang_provider,
@ -141,7 +144,7 @@ impl<T: 'static> Frontend<T> {
family_name_monospace: "",
},
PathBuf::new(), //FIXME: pass from somewhere else
params.color_palette,
palette,
)?;
let (layout, state) = wgui::parser::new_layout_from_assets(

View File

@ -6,11 +6,15 @@ use wgui::{
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
log::LogErr,
palette::PALETTES,
parser::{Fetchable, ParseDocumentParams, TemplateParams},
task::Tasks,
};
use wlx_common::dash_interface::ConfigChangeKind;
use wlx_common::{
dash_interface::ConfigChangeKind,
palette::{list_palette_files, load_custom_palette},
};
use crate::{
frontend::{FrontendTask, FrontendTasks},
@ -22,6 +26,7 @@ use crate::{
#[derive(Clone)]
enum Task {
SelectPalette(String),
CustomPaletteUrl,
Restart,
Cancel,
}
@ -66,6 +71,11 @@ impl ViewTrait for View {
self.settings_tasks.push(SettingsTask::RestartSoftware);
}
Task::CustomPaletteUrl => {
self.frontend_tasks.push(FrontendTask::OpenURL(
"https://wayvr.org/docs/basics/customization/".into(),
));
}
}
}
Ok(())
@ -105,8 +115,67 @@ impl View {
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 {
continue;
};
let id = format!("profile_custom_{idx}");
let is_current = &*params.current_palette == name.as_str();
let mut cell_params = TemplateParams::new();
cell_params.insert("id", &id);
let display_name = &name[..name.len() - 5];
if is_current {
cell_params.insert_str("text", format!("{display_name}"));
cell_params.insert("tooltip", "APP_SETTINGS.COLOR_PALETTE_CURRENT");
} else {
cell_params.insert("text", display_name);
cell_params.insert("tooltip", "APP_SETTINGS.COLOR_PALETTE_ACTIVATE");
}
insert_colors!(
cell_params,
&palette,
"primary" => Primary,
"on_primary" => OnPrimary,
"secondary" => Secondary,
"on_secondary" => OnSecondary,
"tertiary" => Tertiary,
"on_tertiary" => OnTertiary,
"danger" => Danger,
"on_danger" => OnDanger,
"background" => Background,
"on_background" => OnBackground,
"background_variant" => BackgroundVariant,
"outline" => Outline,
"highlight" => Highlight,
);
parser_state.instantiate_template(
doc_params,
"ColorPaletteButton",
params.layout,
list_parent,
cell_params,
)?;
if !is_current {
let btn = parser_state.fetch_component_as::<ComponentButton>(&id)?;
let tasks_clone = tasks.clone();
btn.on_click(Rc::new({
move |_common, _e| {
tasks_clone.push(Task::SelectPalette(name.to_string()));
Ok(())
}
}));
}
}
for (idx, (name, palette)) in PALETTES.iter().enumerate() {
let id = format!("profile_btn_{idx}");
let id = format!("profile_builtin_{idx}");
let is_current = &*params.current_palette == *name;
let mut cell_params = TemplateParams::new();
@ -158,6 +227,22 @@ impl View {
}
}
parser_state.instantiate_template(
doc_params,
"CustomPaletteButton",
params.layout,
list_parent,
TemplateParams::default(),
)?;
let btn = parser_state.fetch_component_as::<ComponentButton>("custom_btn")?;
let tasks_clone = tasks.clone();
btn.on_click(Rc::new({
move |_common, _e| {
tasks_clone.push(Task::CustomPaletteUrl);
Ok(())
}
}));
Ok(Self {
tasks,
frontend_tasks: params.frontend_tasks.clone(),

View File

@ -10,6 +10,7 @@ use wgui::{
font_config::WguiFontConfig,
globals::WguiGlobals,
layout::{Layout, LayoutParams, LayoutUpdateParams},
palette::WguiColorPalette,
parser::{ParseDocumentParams, ParserState},
};
use wlx_common::locale::WayVRLangProvider;
@ -37,7 +38,7 @@ impl TestbedAny {
&lang_provider,
&WguiFontConfig::default(),
PathBuf::new(), // cwd
&palette_name,
WguiColorPalette::get_builtin(&palette_name),
)?;
let (layout, state) = wgui::parser::new_layout_from_assets(

View File

@ -8,15 +8,16 @@ use glam::Vec2;
use wgui::{
assets::AssetPath,
components::{
Component,
button::{ButtonClickCallback, ComponentButton},
checkbox::ComponentCheckbox,
Component,
},
drawing::Color,
font_config::WguiFontConfig,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, LayoutParams, LayoutUpdateParams, Widget},
palette::WguiColorPalette,
parser::{Fetchable, ParseDocumentExtra, ParseDocumentParams, ParserState},
taffy::{self, prelude::length},
task::Tasks,
@ -93,7 +94,7 @@ impl TestbedGeneric {
&lang_provider,
&WguiFontConfig::default(),
PathBuf::new(), // cwd
&palette_name,
WguiColorPalette::get_builtin(&palette_name),
)?;
let extra = ParseDocumentExtra {

View File

@ -16,6 +16,7 @@ use wgui::{
use wlx_capture::pipewire::ScreenCastManager;
use wlx_common::config::PwTokenMap;
use wlx_common::locale::WayVRLangProvider;
use wlx_common::palette::load_palette;
use wlx_common::{
audio,
config::GeneralConfig,
@ -167,6 +168,8 @@ impl AppState {
)
.ok();
let palette = load_palette(&*session.config.color_palette);
let mut app_state = Self {
tasks,
gfx,
@ -184,7 +187,7 @@ impl AppState {
&lang_provider,
&WguiFontConfig::default(),
get_config_file_path(&theme_path),
&session.config.color_palette,
palette,
)?,
wgui_theme: Rc::new(theme),
dbus,

View File

@ -37,7 +37,7 @@ impl WguiGlobals {
lang_provider: &dyn LangProvider,
font_config: &WguiFontConfig,
asset_folder: PathBuf,
color_palette: &str,
palette: WguiColorPalette,
) -> anyhow::Result<Self> {
let i18n_builtin = I18n::new(assets_builtin.as_mut(), lang_provider)?;
let assets_internal = Box::new(assets_internal::AssetInternal {});
@ -49,7 +49,7 @@ impl WguiGlobals {
font_system: WguiFontSystem::new(font_config, i18n_builtin.get_locale()),
i18n_builtin,
custom_glyph_cache: CustomGlyphCache::new(),
palette: WguiColorPalette::from_name(color_palette),
palette,
}))))
}

View File

@ -13,7 +13,7 @@ pub struct WguiColorPalette {
}
impl WguiColorPalette {
pub fn from_name(palette_name: &str) -> Self {
pub fn get_builtin(palette_name: &str) -> Self {
for (name, palette) in PALETTES {
if *name == palette_name {
// just clone here, makes it easier for future customization
@ -25,6 +25,10 @@ impl WguiColorPalette {
return DEFAULT.clone();
}
pub const fn from_inner(colors: [drawing::Color; WguiColorName::COUNT]) -> Self {
Self { colors }
}
fn apply_modifier(color: WguiColor, modifier: &str) -> Option<WguiColor> {
match modifier {
"transparent" => Some(color.with_alpha(0.0)),
@ -82,8 +86,8 @@ const fn hex(hex: &str) -> drawing::Color {
pub static PALETTES: &[(&'static str, &WguiColorPalette)] = &[
("Default", DEFAULT),
("Ayu", AYU),
("Catppuccin", CATTPUCCIN),
("Ayu Dusk", AYU),
("Catppuccin", CATPPUCCIN),
("Cyberpunk", CYBERPUNK),
("Dracula", DRACULA),
("Eldritch", ELDRITCH),
@ -93,21 +97,21 @@ pub static PALETTES: &[(&'static str, &WguiColorPalette)] = &[
("Monochrome", MONOCHROME),
("Nord", NORD),
("Osaka Jade", OSAKA_JADE),
("Rosepine", ROSEPINE),
("Rosé Pine", ROSEPINE),
("Solarized Night", SOLARIZED_NIGHT),
("Tokyo Night", TOKYO_NIGHT),
("Ayu Light", AYU_LIGHT),
("Catppuccin Light", CATTPUCCIN_LIGHT),
("Ayu Day", AYU_LIGHT),
("Catppuccin Latte", CATPPUCCIN_LATTE),
("Cyberpunk Light", CYBERPUNK_LIGHT),
("Dracula Light", DRACULA_LIGHT),
("Eldritch Light", ELDRITCH_LIGHT),
("Alucard", DRACULA_LIGHT),
("Eldritch Dusk", ELDRITCH_LIGHT),
("Everforest Light", EVERFOREST_LIGHT),
("Gruvbox Light", GRUVBOX_LIGHT),
("Kanagawa Light", KANAGAWA_LIGHT),
("Kanagawa Lotus", KANAGAWA_LIGHT),
("Monochrome Light", MONOCHROME_LIGHT),
("Nord Light", NORD_LIGHT),
("Osaka Jade Light", OSAKA_JADE_LIGHT),
("Rosepine Light", ROSEPINE_LIGHT),
("Rosé Pine Dawn", ROSEPINE_LIGHT),
("Solarized Day", SOLARIZED_DAY),
("Tokyo Day", TOKYO_DAY),
];
@ -178,7 +182,7 @@ static AYU_LIGHT: &WguiColorPalette = &WguiColorPalette {
],
};
static CATTPUCCIN: &WguiColorPalette = &WguiColorPalette {
static CATPPUCCIN: &WguiColorPalette = &WguiColorPalette {
colors: [
hex("#cba6f7"), // Primary
hex("#11111b"), // OnPrimary
@ -200,13 +204,13 @@ static CATTPUCCIN: &WguiColorPalette = &WguiColorPalette {
],
};
static CATTPUCCIN_LIGHT: &WguiColorPalette = &WguiColorPalette {
static CATPPUCCIN_LATTE: &WguiColorPalette = &WguiColorPalette {
colors: [
hex("#8839EF"), // Primary
hex("#EFF1F5"), // OnPrimary
hex("#FE640B"), // Secondary
hex("#EFF1F5"), // OnSecondary
hex("#40A02B"), // Tertiary
hex("#179299"), // Tertiary
hex("#EFF1F5"), // OnTertiary
hex("#D20F39"), // Danger
hex("#DCE0E8"), // OnDanger

View File

@ -25,6 +25,10 @@ pub fn get_skymaps_root() -> PathBuf {
get_config_root().join("skymaps")
}
pub fn get_palettes_root() -> PathBuf {
get_config_root().join("palettes")
}
pub fn get_skymaps_uuids() -> anyhow::Result<Vec<String>> {
let data = std::fs::read_to_string(get_skymaps_root().join("skymaps.txt"))?;
Ok(data.lines().filter(|line| !line.is_empty()).map(String::from).collect())

View File

@ -15,5 +15,6 @@ pub mod openxr_actions;
pub mod openxr_bindings_schema;
pub mod openxr_controller_profiles;
pub mod overlays;
pub mod palette;
pub mod timestep;
pub mod windowing;

153
wlx-common/src/palette.rs Normal file
View File

@ -0,0 +1,153 @@
use std::{fs::File, io::BufReader};
use serde::{de::Error as _, Deserialize, Deserializer};
use wgui::{
drawing,
log::LogErr,
palette::{WguiColorPalette, PALETTES},
};
use crate::config_io;
pub fn load_palette(want_palette: &str) -> WguiColorPalette {
if is_custom_palette(want_palette) {
if let Ok(palette) = load_custom_palette(want_palette).log_warn("Could not load custom color palette") {
return palette;
}
} else {
for (name, palette) in PALETTES {
if *name == want_palette {
return (*palette).clone();
}
}
log::warn!("No color palette found with name: {want_palette}")
}
PALETTES[0].1.clone()
}
fn is_custom_palette(name: &str) -> bool {
name
.as_bytes()
.get(name.len().saturating_sub(5)..)
.is_some_and(|suffix| suffix.eq_ignore_ascii_case(b".json"))
}
pub fn list_palette_files() -> Vec<String> {
let path = config_io::get_palettes_root();
let Ok(entries) = std::fs::read_dir(path) else {
return Vec::new();
};
let mut files: Vec<_> = entries
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
if !path.is_file() || !path.extension()?.to_str()?.eq_ignore_ascii_case("json") {
return None;
}
Some(path.file_name()?.to_str()?.to_owned())
})
.collect();
files.sort_unstable();
files
}
pub fn load_custom_palette(name: &str) -> anyhow::Result<WguiColorPalette> {
let path = config_io::get_palettes_root().join(name);
let file = File::open(path)?;
let reader = BufReader::new(file);
let scp = serde_json::from_reader::<_, SerializedWguiColorPalette>(reader)?;
Ok(scp.into_palette())
}
fn deserialize_color<'de, D>(deserializer: D) -> Result<drawing::Color, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
drawing::Color::from_hex(&value).ok_or_else(|| D::Error::custom(format!("invalid hexadecimal color: {value:?}")))
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SerializedWguiColorPalette {
#[serde(deserialize_with = "deserialize_color")]
primary: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
on_primary: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
secondary: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
on_secondary: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
tertiary: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
on_tertiary: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
danger: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
on_danger: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
background: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
on_background: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
background_variant: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
on_background_variant: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
background_contrast: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
on_background_contrast: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
outline: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
shadow: drawing::Color,
#[serde(deserialize_with = "deserialize_color")]
highlight: drawing::Color,
}
impl SerializedWguiColorPalette {
pub fn into_palette(&self) -> WguiColorPalette {
// order must stay the same as per WguiColorName
WguiColorPalette::from_inner([
self.primary,
self.on_primary,
self.secondary,
self.on_secondary,
self.tertiary,
self.on_tertiary,
self.danger,
self.on_danger,
self.background,
self.on_background,
self.background_variant,
self.on_background_variant,
self.background_contrast,
self.on_background_contrast,
self.outline,
self.shadow,
self.highlight,
])
}
}