Fully functional audio settings, add sprite_src for <Button>

This commit is contained in:
Aleksander
2025-12-06 12:08:25 +01:00
parent e83997bc08
commit bc5075a732
14 changed files with 457 additions and 28 deletions

View File

@@ -1,7 +1,7 @@
<layout>
<include src="../t_group_box.xml" />
<!-- device_name, device_icon, id_checkbox -->
<!-- device_name, device_icon -->
<template name="DeviceSlider">
<rectangle macro="group_box">
<div width="100%" align_items="center" justify_content="center" gap="8">
@@ -10,34 +10,55 @@
</div>
<div width="100%" align_items="center">
<CheckBox id="checkbox" />
<Button id="btn_mute">
<sprite src="${volume_icon}" width="20" height="20" margin="4" margin_left="8" margin_right="8" />
</Button>
<Button sprite_src="${volume_icon}" id="btn_mute" width="32" />
<Slider id="slider" flex_grow="1" height="16" min_value="0" max_value="150" margin_left="8" />
</div>
</rectangle>
</template>
<!-- card_name, profile_name -->
<template name="Card">
<rectangle macro="group_box">
<div width="100%" align_items="center" justify_content="center">
<label text="${card_name}" size="12" weight="bold" />
</div>
<Button id="btn_card" text="${profile_name}" width="100%" height="32" />
</rectangle>
</template>
<template name="SelectAudioProfileText">
<div align_items="center" gap="8">
<Button width="48" height="32" id="btn_back">
<sprite src="dashboard/back.svg" width="24" height="24" />
</Button>
<label translation="AUDIO.SELECT_AUDIO_CARD_PROFILE" size="14" weight="bold" />
</div>
</template>
<!-- id (Button), src, translation -->
<template name="BottomButton">
<Button flex_grow="1" id="${id}">
<sprite src="${src}"
min_width="24" min_height="24" width="24" height="24" margin="4" margin_left="16" />
<label translation="${translation}" weight="bold" margin_right="16" />
<Button
flex_grow="1"
id="${id}"
translation="${translation}"
sprite_src="${src}">
</Button>
</template>
<elements>
<div id="devices" flex_direction="column" gap="8">
<div id="devices" flex_direction="column" gap="4">
</div>
<!-- bottom buttons -->
<div flex_direction="row" gap="4">
<Button tooltip="AUDIO.AUTO_SWITCH_TO_VR_AUDIO" color="#00CCFF" tooltip_side="right">
<sprite src="dashboard/magic_wand.svg"
min_width="24" width="24" height="24" margin="4" />
</Button>
<Button
id="btn_auto"
sprite_src="dashboard/magic_wand.svg"
min_width="32"
tooltip="AUDIO.AUTO_SWITCH_TO_VR_AUDIO"
color="~color_accent"
tooltip_side="right" />
<BottomButton id="btn_sinks" src="dashboard/volume.svg" translation="AUDIO.SPEAKERS" />
<BottomButton id="btn_sources" src="dashboard/microphone.svg" translation="AUDIO.MICROPHONES" />

View File

@@ -34,7 +34,8 @@
"AUTO_SWITCH_TO_VR_AUDIO": "Automatisch auf VR-Audio umschalten",
"SPEAKERS": "Lautsprecher",
"MICROPHONES": "Mikrofone",
"CARDS": "Karten"
"CARDS": "Karten",
"SELECT_AUDIO_CARD_PROFILE": "Wählen Sie das Audio-Kartenprofil"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Playspace neu zentrieren"

View File

@@ -29,6 +29,7 @@
}
},
"AUDIO": {
"SELECT_AUDIO_CARD_PROFILE": "Select audio card profile",
"SETTINGS": "Audio settings",
"VOLUME": "Volume",
"AUTO_SWITCH_TO_VR_AUDIO": "Auto-switch to VR audio",

View File

@@ -34,7 +34,8 @@
"AUTO_SWITCH_TO_VR_AUDIO": "Conmutar automáticamente al audio VR",
"SPEAKERS": "Altavoces",
"MICROPHONES": "Micrófonos",
"CARDS": "Tarjetas"
"CARDS": "Tarjetas",
"SELECT_AUDIO_CARD_PROFILE": "Seleccionar perfil de tarjeta de audio"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Re-centrar espacio de juego"

View File

@@ -34,7 +34,8 @@
"AUTO_SWITCH_TO_VR_AUDIO": "VRオーディオに自動切り替え",
"SPEAKERS": "スピーカー",
"MICROPHONES": "マイク",
"CARDS": "カード"
"CARDS": "カード",
"SELECT_AUDIO_CARD_PROFILE": "オーディオカードプロファイルを選択"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "プレイスペースを再中央"

View File

@@ -34,7 +34,8 @@
"AUTO_SWITCH_TO_VR_AUDIO": "Automatyczne przełączanie na dźwięk VR",
"SPEAKERS": "Głośniki",
"MICROPHONES": "Mikrofony",
"CARDS": "Karty"
"CARDS": "Karty",
"SELECT_AUDIO_CARD_PROFILE": "Wybierz profil karty dźwiękowej"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Wycentruj przestrzeń"

View File

@@ -70,6 +70,7 @@ pub enum FrontendTask {
ShowAudioSettings,
UpdateAudioSettingsView,
RecenterPlayspace,
PushToast(String),
}
impl Frontend {
@@ -252,6 +253,7 @@ impl Frontend {
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
FrontendTask::RecenterPlayspace => self.action_recenter_playspace()?,
FrontendTask::PushToast(text) => self.push_toast(text)?,
}
Ok(())
}
@@ -383,6 +385,7 @@ impl Frontend {
self.view_audio_settings = Some(views::audio_settings::View::new(views::audio_settings::Params {
globals: self.globals.clone(),
frontend_tasks: self.tasks.clone(),
layout: &mut layout,
parent_id: content.id,
on_update: {
@@ -410,4 +413,9 @@ impl Frontend {
log::info!("todo");
Ok(())
}
fn push_toast(&mut self, text: String) -> anyhow::Result<()> {
log::info!("TODO toast: {}", text);
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use serde::{Deserialize, Serialize};
@@ -98,8 +98,8 @@ pub struct Card {
pub name: String, // alsa_card.pci-0000_0c_00.4
pub active_profile: String, // output:analog-stereo
pub properties: CardProperties,
pub profiles: HashMap<String, CardProfile>, // key: "output:analog-stereo"
pub ports: HashMap<String, CardPort>, // key: "analog-output-lineout"
pub profiles: BTreeMap<String, CardProfile>, // key: "output:analog-stereo"
pub ports: BTreeMap<String, CardPort>, // key: "analog-output-lineout"
}
// ########################################

View File

@@ -3,22 +3,37 @@ use std::{collections::HashMap, rc::Rc};
use wgui::{
assets::AssetPath,
components::{
self,
button::{ButtonClickCallback, ComponentButton},
checkbox::ComponentCheckbox,
slider::ComponentSlider,
},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
widget::ConstructEssentials,
};
use crate::{task::Tasks, util::pactl_wrapper};
use crate::{
frontend::{FrontendTask, FrontendTasks},
task::Tasks,
util::pactl_wrapper,
};
#[derive(Clone)]
#[allow(clippy::large_enum_variant)]
enum CurrentMode {
Sinks,
Sources,
Cards,
CardProfileSelector(pactl_wrapper::Card),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SearchType {
Sink,
Source,
}
#[derive(Clone)]
@@ -28,17 +43,27 @@ struct IndexAndVolume {
}
#[derive(Clone)]
struct CardAndProfileName {
card: pactl_wrapper::Card,
profile_name: String,
}
#[derive(Clone)]
#[allow(clippy::large_enum_variant)]
enum ViewTask {
Remount,
SetMode(CurrentMode),
AutoSwitch,
SetSinkVolume(IndexAndVolume),
SetSourceVolume(IndexAndVolume),
SetCardProfile(CardAndProfileName),
}
type ViewTasks = Tasks<ViewTask>;
pub struct View {
tasks: ViewTasks,
frontend_tasks: FrontendTasks, // used only for toasts
on_update: Rc<dyn Fn()>,
globals: WguiGlobals,
@@ -54,6 +79,7 @@ pub struct View {
pub struct Params<'a> {
pub globals: WguiGlobals,
pub frontend_tasks: FrontendTasks,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub on_update: Rc<dyn Fn()>,
@@ -65,6 +91,51 @@ struct ProfileDisplayName {
is_vr: bool,
}
struct SelectorCell {
key: String,
display_text: String,
icon_path: &'static str,
}
struct MultiSelectorParams<'a> {
cells: &'a [SelectorCell],
def_cell: &'a str,
ess: &'a mut ConstructEssentials<'a>,
on_click: Rc<dyn Fn(&str /* key */)>,
}
fn mount_multi_selector(params: MultiSelectorParams) -> anyhow::Result<()> {
let globals = params.ess.layout.state.globals.clone();
let accent_color = globals.get().defaults.accent_color;
for cell in params.cells {
let highlighted = cell.key == params.def_cell;
let color = if highlighted { Some(accent_color) } else { None };
// button
let (_, button) = components::button::construct(
params.ess,
components::button::Params {
text: Some(Translation::from_raw_text(&cell.display_text)),
sprite_src: Some(AssetPath::BuiltIn(cell.icon_path)),
color,
..Default::default()
},
)?;
button.on_click({
let on_click = params.on_click.clone();
let key = cell.key.clone();
Box::new(move |_, _| {
(*on_click)(key.as_str());
Ok(())
})
});
}
Ok(())
}
fn get_card_from_sink<'a>(
sink: &pactl_wrapper::Sink,
cards: &'a [pactl_wrapper::Card],
@@ -325,6 +396,172 @@ struct MountDeviceSliderParams<'a> {
alt_desc: String,
}
fn switch_sink_card(
frontend_tasks: &FrontendTasks,
card: &pactl_wrapper::Card,
profile_name: &str,
name: &ProfileDisplayName,
) -> anyhow::Result<()> {
let card_index = card.index;
let profile = profile_name.to_string();
pactl_wrapper::set_card_profile(card_index, &profile)?;
let sinks = pactl_wrapper::list_sinks()?;
let mut sink_found = false;
for sink in &sinks {
if let Some(device_name) = sink.properties.get("device.name")
&& device_name == &card.name
{
pactl_wrapper::set_default_sink(sink.index)?;
sink_found = true;
break;
}
}
if sink_found {
frontend_tasks.push(FrontendTask::PushToast(format!(
"Speakers set to \"{}\" successfully!",
name.name
)));
} else {
frontend_tasks.push(FrontendTask::PushToast(format!(
"\"{}\" found and initialized! (not switched)",
name.name
)));
}
Ok(())
}
fn switch_source(frontend_tasks: &FrontendTasks, source: &pactl_wrapper::Source) -> anyhow::Result<()> {
match pactl_wrapper::set_default_source(source.index) {
Ok(()) => {
frontend_tasks.push(FrontendTask::PushToast(format!(
"Microphone set to \"{}\" successfully!",
if let Some(card_name) = &source.properties.card_name {
card_name
} else {
&source.description
}
)));
Ok(())
}
Err(e) => {
frontend_tasks.push(FrontendTask::PushToast(format!("Failed to switch microphone: {:?}", e)));
Err(e)
}
}
}
fn switch_to_vr_microphone(frontend_tasks: &FrontendTasks) -> anyhow::Result<()> {
let sources = pactl_wrapper::list_sources()?;
let mut switched = false;
for source in &sources {
if is_source_mentioning_hmd(source) {
switch_source(frontend_tasks, source)?;
switched = true;
break;
}
}
if !switched {
frontend_tasks.push(FrontendTask::PushToast(
"No VR microphone found. Switch it manually.".to_string(),
));
}
Ok(())
}
#[derive(Debug, Clone)]
struct CardPriorityResult<'a> {
priority: u32,
name: String,
card: &'a pactl_wrapper::Card,
}
fn get_card_best_profile<'a>(card: &'a pactl_wrapper::Card, search_type: SearchType) -> Option<CardPriorityResult<'a>> {
let mut best_priority = 0;
let mut best_profile_name = "";
let mut best_profile: Option<&'a pactl_wrapper::CardProfile> = None;
for (profile_name, profile) in &card.profiles {
match search_type {
SearchType::Sink if profile.sinks == 0 => continue,
SearchType::Source if profile.sources == 0 => continue,
_ => {}
}
if profile.priority > best_priority {
best_priority = profile.priority;
best_profile = Some(profile);
best_profile_name = profile_name;
}
}
best_profile?; // do not proceed if no profile was found
Some(CardPriorityResult {
priority: best_priority,
name: best_profile_name.to_string(),
card,
})
}
fn get_best_profile_from_array<'a>(arr: &[CardPriorityResult<'a>]) -> Option<CardPriorityResult<'a>> {
let mut best_priority = 0;
let mut res: Option<CardPriorityResult<'a>> = None;
for cell in arr {
if cell.priority > best_priority {
best_priority = cell.priority;
res = Some(cell.clone());
}
}
res
}
fn switch_to_vr_speakers(frontend_tasks: &FrontendTasks) -> anyhow::Result<()> {
let cards = pactl_wrapper::list_cards()?;
let mut best_profiles = Vec::new();
for card in &cards {
if !is_card_mentioning_hmd(card) {
continue;
}
if let Some(best_profile) = get_card_best_profile(card, SearchType::Sink) {
best_profiles.push(best_profile);
}
}
if !best_profiles.is_empty() {
let best_profile = get_best_profile_from_array(&best_profiles).unwrap();
let name = get_profile_display_name(&best_profile.name, best_profile.card);
switch_sink_card(frontend_tasks, best_profile.card, &best_profile.name, &name)?;
return Ok(());
}
// There aren't any cards which mention VR explicitly. Time for plan B.
for card in &cards {
for profile_name in card.profiles.keys() {
let name = get_profile_display_name(profile_name, card);
if !name.is_vr {
continue;
}
switch_sink_card(frontend_tasks, card, profile_name, &name)?;
return Ok(());
}
}
frontend_tasks.push(FrontendTask::PushToast(
"No VR speakers found. Switch them manually.".to_string(),
));
Ok(())
}
const ONE_HUNDRED_PERCENT: f32 = 100.0;
const VOLUME_MULT: f32 = 1.0 / ONE_HUNDRED_PERCENT;
@@ -349,9 +586,11 @@ impl View {
let btn_sinks = state.fetch_component_as::<ComponentButton>("btn_sinks")?;
let btn_sources = state.fetch_component_as::<ComponentButton>("btn_sources")?;
let btn_cards = state.fetch_component_as::<ComponentButton>("btn_cards")?;
let btn_auto = state.fetch_component_as::<ComponentButton>("btn_auto")?;
let mut res = Self {
globals: params.globals,
frontend_tasks: params.frontend_tasks,
state,
mode: CurrentMode::Sinks,
id_devices,
@@ -362,6 +601,7 @@ impl View {
btn_sinks.on_click(res.handle_func_button_click(ViewTask::SetMode(CurrentMode::Sinks)));
btn_sources.on_click(res.handle_func_button_click(ViewTask::SetMode(CurrentMode::Sources)));
btn_cards.on_click(res.handle_func_button_click(ViewTask::SetMode(CurrentMode::Cards)));
btn_auto.on_click(res.handle_func_button_click(ViewTask::AutoSwitch));
res.init_mode_sinks(params.layout)?;
@@ -379,10 +619,11 @@ impl View {
for task in tasks {
match task {
ViewTask::Remount => match self.mode {
ViewTask::Remount => match &self.mode {
CurrentMode::Sinks => self.init_mode_sinks(layout)?,
CurrentMode::Sources => self.init_mode_sources(layout)?,
CurrentMode::Cards => self.init_mode_cards(layout)?,
CurrentMode::CardProfileSelector(card) => self.init_mode_card_selector(layout, card.clone())?,
},
ViewTask::SetSinkVolume(s) => {
set_sink_volume = Some(s);
@@ -394,6 +635,14 @@ impl View {
self.mode = current_mode;
self.tasks.push(ViewTask::Remount);
}
ViewTask::SetCardProfile(c) => {
pactl_wrapper::set_card_profile(c.card.index, &c.profile_name)?;
}
ViewTask::AutoSwitch => {
switch_to_vr_microphone(&self.frontend_tasks)?;
switch_to_vr_speakers(&self.frontend_tasks)?;
self.tasks.push(ViewTask::Remount);
}
}
}
@@ -417,6 +666,29 @@ impl View {
}
fn mount_card(&mut self, params: MountCardParams) -> anyhow::Result<()> {
let desc = &params.card.properties.device_description;
let disp_name = get_profile_display_name(&params.card.active_profile, params.card);
let mut par = HashMap::<Rc<str>, Rc<str>>::new();
par.insert("card_name".into(), desc.as_str().into());
par.insert("profile_name".into(), disp_name.name.as_str().into());
let data = self
.state
.parse_template(&doc_params(&self.globals), "Card", params.layout, self.id_devices, par)?;
let btn_card = data.fetch_component_as::<ComponentButton>("btn_card")?;
btn_card.on_click({
let tasks = self.tasks.clone();
let card = params.card.clone();
let on_update = self.on_update.clone();
Box::new(move |_common, _evt| {
tasks.push(ViewTask::SetMode(CurrentMode::CardProfileSelector(card.clone())));
(*on_update)();
Ok(())
})
});
log::info!("mount card TODO: {}", params.card.name);
Ok(())
}
@@ -581,4 +853,70 @@ impl View {
Ok(())
}
fn init_mode_card_selector(&mut self, layout: &mut Layout, card: pactl_wrapper::Card) -> anyhow::Result<()> {
log::info!("showing card selector for {}", card.name);
layout.remove_children(self.id_devices);
{
let data = self.state.parse_template(
&doc_params(&self.globals),
"SelectAudioProfileText",
layout,
self.id_devices,
Default::default(),
)?;
let btn_back = data.fetch_component_as::<ComponentButton>("btn_back")?;
btn_back.on_click({
let tasks = self.tasks.clone();
let on_update = self.on_update.clone();
Box::new(move |_, _| {
tasks.push(ViewTask::SetMode(CurrentMode::Cards));
(*on_update)();
Ok(())
})
});
}
let mut cells = Vec::<SelectorCell>::new();
for profile_name in card.profiles.keys() {
if profile_name.contains("surround") {
continue; // we aren't interested in that
}
let disp_name = get_profile_display_name(profile_name, &card);
cells.push(SelectorCell {
key: profile_name.clone(),
display_text: disp_name.name,
icon_path: disp_name.icon_path,
});
}
let mut ess = ConstructEssentials {
layout,
parent: self.id_devices,
};
mount_multi_selector(MultiSelectorParams {
cells: &cells,
def_cell: &card.active_profile,
ess: &mut ess,
on_click: {
let card = card.clone();
let tasks = self.tasks.clone();
let on_update = self.on_update.clone();
Rc::new(move |profile_name| {
tasks.push(ViewTask::SetCardProfile(CardAndProfileName {
card: card.clone(),
profile_name: profile_name.to_string(),
}));
tasks.push(ViewTask::SetMode(CurrentMode::Cards));
(*on_update)();
})
},
})?;
Ok(())
}
}

View File

@@ -164,6 +164,9 @@ async function run() {
const response = await ollama.chat({
model: model_name,
messages: [{ role: "user", content: prompt }],
options: {
seed: 12345,
}
})
const msg = extract_backticks(response.message.content);

View File

@@ -21,7 +21,7 @@ use wgui::{
parser::{Fetchable, ParseDocumentExtra, ParseDocumentParams, ParserState},
taffy,
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
windowing::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra},
windowing::{WguiWindow, WguiWindowParams},
};
pub enum TestbedTask {

View File

@@ -210,6 +210,10 @@ _Tooltip text on hover, translated by key_
_make button act as a toggle (visual only)_
`sprite_src` | `sprite_src_ext` | `sprite_src_internal`
_Image path (see [sprite](#sprite-widget)) for src descriptions_
#### Info
Child widgets are supported and can be added directly in XML.

View File

@@ -1,18 +1,23 @@
use crate::{
animation::{Animation, AnimationEasing},
assets::AssetPath,
components::{self, Component, ComponentBase, ComponentTrait, RefreshData, tooltip::ComponentTooltip},
drawing::{self, Boundary, Color},
event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind},
i18n::Translation,
layout::{LayoutTask, WidgetID, WidgetPair},
renderer_vk::{
text::{FontWeight, TextStyle},
text::{
FontWeight, TextStyle,
custom_glyph::{CustomGlyphContent, CustomGlyphData},
},
util::centered_matrix,
},
widget::{
self, ConstructEssentials, EventResult, WidgetData,
label::{WidgetLabel, WidgetLabelParams},
rectangle::{WidgetRectangle, WidgetRectangleParams},
sprite::{WidgetSprite, WidgetSpriteParams},
util::WLength,
},
};
@@ -20,8 +25,9 @@ use glam::{Mat4, Vec3};
use std::{cell::RefCell, rc::Rc};
use taffy::{AlignItems, JustifyContent, prelude::length};
pub struct Params {
pub struct Params<'a> {
pub text: Option<Translation>, // if unset, label will not be populated
pub sprite_src: Option<AssetPath<'a>>,
pub color: Option<drawing::Color>,
pub border: f32,
pub border_color: Option<drawing::Color>,
@@ -37,10 +43,11 @@ pub struct Params {
pub sticky: bool,
}
impl Default for Params {
impl Default for Params<'_> {
fn default() -> Self {
Self {
text: Some(Translation::from_raw_text("")),
sprite_src: None,
color: None,
hover_color: None,
border_color: None,
@@ -366,7 +373,7 @@ fn register_event_mouse_release(
#[allow(clippy::too_many_lines)]
pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentButton>)> {
let globals = ess.layout.state.globals.clone();
let mut globals = ess.layout.state.globals.clone();
let mut style = params.style;
// force-override style
@@ -374,7 +381,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
style.justify_content = Some(JustifyContent::Center);
style.overflow.x = taffy::Overflow::Hidden;
style.overflow.y = taffy::Overflow::Hidden;
style.gap = length(4.0);
style.gap = length(8.0);
// update colors to default ones if they are not specified
let color = if let Some(color) = params.color {
@@ -425,6 +432,34 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
(color.r + color.g + color.b) * mult < 1.5
};
if let Some(sprite_path) = params.sprite_src {
let sprite = WidgetSprite::create(WidgetSpriteParams {
glyph_data: Some(CustomGlyphData::new(CustomGlyphContent::from_assets(
&mut globals,
sprite_path,
)?)),
..Default::default()
});
ess.layout.add_child(
root.id,
sprite,
taffy::Style {
min_size: taffy::Size {
width: length(20.0),
height: length(20.0),
},
margin: taffy::Rect {
top: length(4.0),
bottom: length(4.0),
left: length(0.0),
right: length(0.0),
},
..Default::default()
},
)?;
}
let id_label = if let Some(content) = params.text {
let (label, _node_label) = ess.layout.add_child(
id_rect,

View File

@@ -1,4 +1,5 @@
use crate::{
assets::AssetPath,
components::{Component, button, tooltip},
drawing::Color,
i18n::Translation,
@@ -27,6 +28,7 @@ pub fn parse_component_button<'a>(
let mut tooltip: Option<String> = None;
let mut tooltip_side: Option<tooltip::TooltipSide> = None;
let mut sticky: bool = false;
let mut sprite_src: Option<AssetPath> = None;
let mut translation: Option<Translation> = None;
@@ -60,6 +62,18 @@ pub fn parse_component_button<'a>(
"hover_border_color" => {
parse_color_opt(value, &mut hover_border_color);
}
"sprite_src" | "sprite_src_ext" | "sprite_src_internal" => {
let asset_path = match key {
"sprite_src" => AssetPath::BuiltIn(value),
"sprite_src_ext" => AssetPath::Filesystem(value),
"sprite_src_internal" => AssetPath::WguiInternal(value),
_ => unreachable!(),
};
if !value.is_empty() {
sprite_src = Some(asset_path);
}
}
"tooltip" => tooltip = Some(String::from(value)),
"tooltip_side" => {
tooltip_side = match value {
@@ -98,6 +112,7 @@ pub fn parse_component_button<'a>(
text: Translation::from_translation_key(&t),
}),
sticky,
sprite_src,
},
)?;