dropdown for capture_method + random tweaks

This commit is contained in:
galister
2026-01-08 16:57:37 +09:00
parent 5616090fa9
commit e9230f6f9f
19 changed files with 358 additions and 82 deletions

1
Cargo.lock generated
View File

@@ -6930,6 +6930,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"smol", "smol",
"strum",
"walkdir", "walkdir",
"wayvr-ipc", "wayvr-ipc",
"wgui", "wgui",

View File

@@ -0,0 +1 @@
../../../wlx-overlay-s/src/assets/keyboard/down.svg

View File

@@ -0,0 +1,27 @@
<layout>
<include src="theme.xml" />
<macro name="dropdown_button"
flex_direction="row"
border="2"
color="#00000055"
border_color="#FFFFFF66"
justify_content="space_between" />
<!-- id, text, translation, tooltip -->
<template name="DropdownButton">
<label text="${text}" translation="${translation}" />
<Button id="${id}" height="32" tooltip="${tooltip}" >
<div padding_left="8" padding_right="8" min_width="200">
<label id="${id}_value" weight="bold" />
</div>
<div gap="2">
<div padding_top="4" padding_bottom="4">
<rectangle width="2" height="100%" color="#FFFFFF66" />
</div>
<sprite margin_left="-4" width="30" height="30" color="~text_color" src_builtin="dashboard/down.svg" />
</div>
</Button>
</template>
</layout>

View File

@@ -1,6 +1,7 @@
<layout> <layout>
<include src="t_tab_title.xml" /> <include src="t_tab_title.xml" />
<include src="../t_group_box.xml" /> <include src="../t_group_box.xml" />
<include src="../t_dropdown_button.xml" />
<template name="SettingsGroupBox"> <template name="SettingsGroupBox">
<rectangle macro="group_box" id="${id}" flex_grow="1"> <rectangle macro="group_box" id="${id}" flex_grow="1">
@@ -27,7 +28,10 @@
</template> </template>
<template name="DangerButton"> <template name="DangerButton">
<Button id="${id}" color="#AA3333" height="32" width="100%" sprite_src_builtin="${icon}" translation="${translation}" tooltip="${translation}_HELP" /> <Button id="${id}" color="#AA3333" height="32" tooltip="${translation}_HELP" padding="4" gap="8" >
<sprite src_builtin="${icon}" height="24" width="24" />
<label align="left" translation="${translation}" weight="bold" min_width="200" />
</Button>
</template> </template>
<elements> <elements>

View File

@@ -69,7 +69,16 @@
"CLEAR_SAVED_STATE_HELP": "Reset sets & overlay positions", "CLEAR_SAVED_STATE_HELP": "Reset sets & overlay positions",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Prompt for screen selection on next start", "CLEAR_PIPEWIRE_TOKENS_HELP": "Prompt for screen selection on next start",
"DELETE_ALL_CONFIGS_HELP": "Remove all configuration files from conf.d", "DELETE_ALL_CONFIGS_HELP": "Remove all configuration files from conf.d",
"RESTART_SOFTWARE_HELP": "Apply settings that require a restart" "RESTART_SOFTWARE_HELP": "Apply settings that require a restart",
"CAPTURE_METHOD": "Wayland screen capture",
"CAPTURE_METHOD_HELP": "Try changing this if experiencing\nblack or glitchy screens",
"OPTION": {
"PIPEWIRE_HELP": "Fast GPU capture. Recommended",
"PW_FALLBACK_HELP": "Slow. Try in case PipeWire GPU doesn't work",
"SCREENCOPY_HELP": "Slow. Works on: Hyprland, Niri, River, Sway"
}
}, },
"APPLICATION_LAUNCHER": "Application launcher", "APPLICATION_LAUNCHER": "Application launcher",
"APPLICATION_STARTED": "Application started", "APPLICATION_STARTED": "Application started",

View File

@@ -1,13 +1,18 @@
use std::{collections::HashMap, marker::PhantomData, rc::Rc}; use std::{collections::HashMap, marker::PhantomData, rc::Rc, str::FromStr};
use strum::AsRefStr; use glam::Vec2;
use strum::{AsRefStr, EnumProperty, EnumString, VariantArray};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider}, components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider},
event::{CallbackDataCommon, EventAlterables},
i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, WidgetID},
log::LogErr, log::LogErr,
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks, task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu::{self, Blueprint, ContextMenu, TickResult},
}; };
use wlx_common::{config::GeneralConfig, config_io::ConfigRoot}; use wlx_common::{config::GeneralConfig, config_io::ConfigRoot};
@@ -20,6 +25,7 @@ enum Task {
UpdateBool(SettingType, bool), UpdateBool(SettingType, bool),
UpdateFloat(SettingType, f32), UpdateFloat(SettingType, f32),
UpdateInt(SettingType, i32), UpdateInt(SettingType, i32),
OpenContextMenu(Vec2, Vec<context_menu::Cell>),
ClearPipewireTokens, ClearPipewireTokens,
ClearSavedState, ClearSavedState,
DeleteAllConfigs, DeleteAllConfigs,
@@ -30,6 +36,8 @@ pub struct TabSettings<T> {
#[allow(dead_code)] #[allow(dead_code)]
pub state: ParserState, pub state: ParserState,
context_menu: ContextMenu,
tasks: Tasks<Task>, tasks: Tasks<Task>,
marker: PhantomData<T>, marker: PhantomData<T>,
} }
@@ -76,17 +84,56 @@ impl<T> Tab<T> for TabSettings<T> {
frontend.interface.restart(data); frontend.interface.restart(data);
return Ok(()); return Ok(());
} }
Task::OpenContextMenu(position, cells) => {
self.context_menu.open(context_menu::OpenParams {
on_custom_attribs: None,
position,
blueprint: Blueprint::Cells(cells),
});
} }
} }
}
// Dropdown handling
if let TickResult::Action(name) = self.context_menu.tick(&mut frontend.layout, &mut self.state)? {
if let (Some(setting), Some(id), Some(value), Some(text), Some(translated)) = {
let mut s = name.splitn(5, ';');
(s.next(), s.next(), s.next(), s.next(), s.next())
} {
let mut label = self
.state
.fetch_widget_as::<WidgetLabel>(&frontend.layout.state, &format!("{id}_value"))?;
let mut alterables = EventAlterables::default();
let mut common = CallbackDataCommon {
alterables: &mut alterables,
state: &frontend.layout.state,
};
let translation = Translation {
text: text.into(),
translated: translated == "1",
};
label.set_text(&mut common, translation);
let setting = SettingType::from_str(setting).expect("Invalid Enum string");
setting.set_enum(config, value);
changed = true;
}
}
// Notify overlays of the change
if changed { if changed {
frontend.interface.config_changed(data); frontend.interface.config_changed(data);
} }
Ok(()) Ok(())
} }
} }
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
#[derive(Clone, Copy, AsRefStr)] #[derive(Clone, Copy, AsRefStr, EnumString)]
enum SettingType { enum SettingType {
AnimationSpeed, AnimationSpeed,
RoundMultiplier, RoundMultiplier,
@@ -120,6 +167,7 @@ enum SettingType {
HideUsername, HideUsername,
OpaqueBackground, OpaqueBackground,
XwaylandByDefault, XwaylandByDefault,
CaptureMethod,
} }
impl SettingType { impl SettingType {
@@ -173,6 +221,40 @@ impl SettingType {
} }
} }
pub fn set_enum<'a>(self, config: &'a mut GeneralConfig, value: &str) {
match self {
Self::CaptureMethod => {
config.capture_method = wlx_common::config::CaptureMethod::from_str(value).expect("Invalid enum value!")
}
_ => panic!("Requested enum for non-enum SettingType"),
}
}
fn get_enum_title<'a>(self, config: &'a mut GeneralConfig) -> Translation {
match self {
Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method),
_ => panic!("Requested enum for non-enum SettingType"),
}
}
fn get_enum_title_inner<E>(value: E) -> Translation
where
E: EnumProperty + AsRef<str>,
{
value
.get_str("Translation")
.map(|x| Translation::from_translation_key(x))
.or_else(|| value.get_str("Text").map(|x| Translation::from_raw_text(x)))
.unwrap_or_else(|| Translation::from_raw_text(value.as_ref()))
}
fn get_enum_tooltip_inner<E>(value: E) -> Option<Translation>
where
E: EnumProperty + AsRef<str>,
{
value.get_str("Tooltip").map(|x| Translation::from_translation_key(x))
}
/// Ok is translation, Err is raw text /// Ok is translation, Err is raw text
fn get_translation(self) -> Result<&'static str, &'static str> { fn get_translation(self) -> Result<&'static str, &'static str> {
match self { match self {
@@ -208,6 +290,7 @@ impl SettingType {
Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"), Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"),
Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"), Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"),
Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"), Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"),
Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"),
} }
} }
@@ -224,6 +307,7 @@ impl SettingType {
Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"), Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"),
Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"), Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"),
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"), Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
Self::CaptureMethod => Some("APP_SETTINGS.CAPTURE_METHOD_HELP"),
_ => None, _ => None,
} }
} }
@@ -381,6 +465,72 @@ macro_rules! slider_i32 {
}; };
} }
macro_rules! dropdown {
($mp:expr, $root:expr, $setting:expr, $options:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match $setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = $setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
$mp
.parser_state
.instantiate_template($mp.doc_params, "DropdownButton", $mp.layout, $root, params)?;
let setting_str = $setting.as_ref();
let title = $setting.get_enum_title($mp.config);
{
let mut label = $mp
.parser_state
.fetch_widget_as::<WidgetLabel>(&$mp.layout.state, &format!("{id}_value"))?;
label.set_text_simple(&mut $mp.layout.state.globals.get(), title);
}
let btn = $mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Box::new({
let tasks = $mp.tasks.clone();
move |_common, e| {
tasks.push(Task::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
$options
.iter()
.filter_map(|item| {
if item.get_bool("Hidden").unwrap_or(false) {
return None;
}
let value = item.as_ref();
let title = SettingType::get_enum_title_inner(*item);
let tooltip = SettingType::get_enum_tooltip_inner(*item);
let text = &title.text;
let translated = if title.translated { "1" } else { "0" };
Some(context_menu::Cell {
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
title,
tooltip,
attribs: vec![],
})
})
.collect(),
));
Ok(())
}
}));
};
}
macro_rules! button { macro_rules! button {
($mp:expr, $root:expr, $translation:expr, $icon:expr, $task:expr) => { ($mp:expr, $root:expr, $translation:expr, $icon:expr, $task:expr) => {
let id = $mp.idx.to_string(); let id = $mp.idx.to_string();
@@ -474,27 +624,33 @@ impl<T> TabSettings<T> {
checkbox!(mp, c, SettingType::UprightScreenFix); checkbox!(mp, c, SettingType::UprightScreenFix);
checkbox!(mp, c, SettingType::DoubleCursorFix); checkbox!(mp, c, SettingType::DoubleCursorFix);
checkbox!(mp, c, SettingType::ScreenRenderDown); checkbox!(mp, c, SettingType::ScreenRenderDown);
dropdown!(
mp,
c,
SettingType::CaptureMethod,
wlx_common::config::CaptureMethod::VARIANTS
);
let c = category!(mp, root, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/blocks.svg")?; let c = category!(mp, root, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/blocks.svg")?;
button!(
mp,
c,
"APP_SETTINGS.CLEAR_SAVED_STATE",
"dashboard/remove_circle.svg",
Task::ClearSavedState
);
button!( button!(
mp, mp,
c, c,
"APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS", "APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
"dashboard/remove_circle.svg", "dashboard/display.svg",
Task::ClearPipewireTokens Task::ClearPipewireTokens
); );
button!(
mp,
c,
"APP_SETTINGS.CLEAR_SAVED_STATE",
"dashboard/binary.svg",
Task::ClearSavedState
);
button!( button!(
mp, mp,
c, c,
"APP_SETTINGS.DELETE_ALL_CONFIGS", "APP_SETTINGS.DELETE_ALL_CONFIGS",
"dashboard/remove_circle.svg", "dashboard/circle.svg",
Task::DeleteAllConfigs Task::DeleteAllConfigs
); );
button!( button!(
@@ -509,6 +665,7 @@ impl<T> TabSettings<T> {
tasks: mp.tasks, tasks: mp.tasks,
state: parser_state, state: parser_state,
marker: PhantomData, marker: PhantomData,
context_menu: ContextMenu::default(),
}) })
} }
} }

View File

@@ -259,8 +259,10 @@ impl TestbedGeneric {
on_custom_attribs: Some(Rc::new(move |custom_attribs| { on_custom_attribs: Some(Rc::new(move |custom_attribs| {
log::info!("custom attribs {:?}", custom_attribs.pairs); log::info!("custom attribs {:?}", custom_attribs.pairs);
})), })),
blueprint: context_menu::Blueprint::Template {
template_name: "my_context_menu".into(), template_name: "my_context_menu".into(),
template_params: Default::default(), template_params: Default::default(),
},
position, position,
}); });
} }

View File

@@ -1,7 +1,7 @@
<layout> <layout>
<!-- text: str --> <!-- text: str -->
<template name="Cell"> <template name="Cell">
<Button id="button" text="${text}" weight="bold" border="0" padding="4" color="#FFFFFF00" /> <Button id="button" text="${text}" tooltip="${tooltip}" weight="bold" border="0" padding="4" color="#FFFFFF00" />
</template> </template>
<template name="Separator"> <template name="Separator">

View File

@@ -1,10 +1,10 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use crate::{ use crate::{
components::{Component, ComponentBase, ComponentTrait, RefreshData, checkbox::ComponentCheckbox}, components::{checkbox::ComponentCheckbox, Component, ComponentBase, ComponentTrait, RefreshData},
event::CallbackDataCommon, event::CallbackDataCommon,
layout::WidgetPair, layout::WidgetPair,
widget::{ConstructEssentials, div::WidgetDiv}, widget::{div::WidgetDiv, ConstructEssentials},
}; };
pub struct RadioValueChangeEvent { pub struct RadioValueChangeEvent {

View File

@@ -9,11 +9,11 @@ use crate::{
layout::{self, LayoutTask, LayoutTasks, WidgetID, WidgetPair}, layout::{self, LayoutTask, LayoutTasks, WidgetID, WidgetPair},
renderer_vk::text::{FontWeight, TextStyle}, renderer_vk::text::{FontWeight, TextStyle},
widget::{ widget::{
ConstructEssentials,
div::WidgetDiv, div::WidgetDiv,
label::{WidgetLabel, WidgetLabelParams}, label::{WidgetLabel, WidgetLabelParams},
rectangle::{WidgetRectangle, WidgetRectangleParams}, rectangle::{WidgetRectangle, WidgetRectangleParams},
util::WLength, util::WLength,
ConstructEssentials,
}, },
}; };

View File

@@ -259,11 +259,11 @@ impl ParserState {
Ok(()) Ok(())
} }
pub(crate) fn context_menu_create_blueprint( pub(crate) fn context_menu_parse_cells(
&mut self, &mut self,
template_name: &str, template_name: &str,
template_params: &HashMap<Rc<str>, Rc<str>>, template_params: &HashMap<Rc<str>, Rc<str>>,
) -> anyhow::Result<context_menu::Blueprint> { ) -> anyhow::Result<Vec<context_menu::Cell>> {
let Some(template) = self.data.templates.get(template_name) else { let Some(template) = self.data.templates.get(template_name) else {
anyhow::bail!("no template named \"{template_name}\" found"); anyhow::bail!("no template named \"{template_name}\" found");
}; };
@@ -283,6 +283,7 @@ impl ParserState {
"" => {} "" => {}
"cell" => { "cell" => {
let mut title: Option<Translation> = None; let mut title: Option<Translation> = None;
let mut tooltip: Option<Translation> = None;
let mut action_name: Option<Rc<str>> = None; let mut action_name: Option<Rc<str>> = None;
let mut attribs = Vec::<AttribPair>::new(); let mut attribs = Vec::<AttribPair>::new();
@@ -292,6 +293,8 @@ impl ParserState {
match key { match key {
"text" => title = Some(Translation::from_raw_text(value)), "text" => title = Some(Translation::from_raw_text(value)),
"translation" => title = Some(Translation::from_translation_key(value)), "translation" => title = Some(Translation::from_translation_key(value)),
"tooltip" => tooltip = Some(Translation::from_translation_key(value)),
"tooltip_str" => tooltip = Some(Translation::from_raw_text(value)),
"action" => action_name = Some(value.into()), "action" => action_name = Some(value.into()),
other => { other => {
if !other.starts_with('_') { if !other.starts_with('_') {
@@ -305,6 +308,7 @@ impl ParserState {
let title = title.context("No text/translation provided")?; let title = title.context("No text/translation provided")?;
cells.push(context_menu::Cell { cells.push(context_menu::Cell {
title, title,
tooltip,
action_name, action_name,
attribs, attribs,
}); });
@@ -316,9 +320,7 @@ impl ParserState {
} }
Ok( Ok(
context_menu::Blueprint {
cells, cells,
}
) )
} }
} }

View File

@@ -15,19 +15,23 @@ use crate::{
pub struct Cell { pub struct Cell {
pub title: Translation, pub title: Translation,
pub tooltip: Option<Translation>,
pub action_name: Option<Rc<str>>, pub action_name: Option<Rc<str>>,
pub attribs: Vec<parser::AttribPair>, pub attribs: Vec<parser::AttribPair>,
} }
pub(crate) struct Blueprint { pub enum Blueprint {
pub cells: Vec<Cell>, Cells(Vec<Cell>),
Template {
template_name: Rc<str>,
template_params: HashMap<Rc<str>, Rc<str>>,
},
} }
pub struct OpenParams { pub struct OpenParams {
pub on_custom_attribs: Option<parser::OnCustomAttribsFunc>, pub on_custom_attribs: Option<parser::OnCustomAttribsFunc>,
pub template_name: Rc<str>,
pub template_params: HashMap<Rc<str>, Rc<str>>,
pub position: Vec2, pub position: Vec2,
pub blueprint: Blueprint,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -74,11 +78,17 @@ impl ContextMenu {
fn open_process( fn open_process(
&mut self, &mut self,
params: &mut OpenParams, params: OpenParams,
layout: &mut Layout, layout: &mut Layout,
parser_state: &mut ParserState, parser_state: &mut ParserState,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let blueprint = parser_state.context_menu_create_blueprint(&params.template_name, &params.template_params)?; let cells = match params.blueprint {
Blueprint::Template {
template_name,
template_params,
} => parser_state.context_menu_parse_cells(&template_name, &template_params)?,
Blueprint::Cells(cells) => cells,
};
let globals = layout.state.globals.clone(); let globals = layout.state.globals.clone();
@@ -100,9 +110,13 @@ impl ContextMenu {
let id_buttons = inner_parser.get_widget_id("buttons")?; let id_buttons = inner_parser.get_widget_id("buttons")?;
for (idx, cell) in blueprint.cells.iter().enumerate() { for (idx, cell) in cells.iter().enumerate() {
let mut par = HashMap::new(); let mut par = HashMap::new();
par.insert(Rc::from("text"), cell.title.generate(&mut globals.i18n())); par.insert(Rc::from("text"), cell.title.generate(&mut globals.i18n()));
if let Some(tooltip) = cell.tooltip.as_ref() {
par.insert(Rc::from("tooltip_str"), tooltip.generate(&mut globals.i18n()));
}
let mut data_cell = inner_parser.parse_template(&doc_params, "Cell", layout, id_buttons, par)?; let mut data_cell = inner_parser.parse_template(&doc_params, "Cell", layout, id_buttons, par)?;
let button = data_cell.fetch_component_as::<ComponentButton>("button")?; let button = data_cell.fetch_component_as::<ComponentButton>("button")?;
@@ -112,7 +126,7 @@ impl ContextMenu {
.tasks .tasks
.handle_button(&button, Task::ActionClicked(cell.action_name.clone())); .handle_button(&button, Task::ActionClicked(cell.action_name.clone()));
if let Some(c) = &mut params.on_custom_attribs { if let Some(c) = params.on_custom_attribs.as_ref() {
(*c)(parser::CustomAttribsInfo { (*c)(parser::CustomAttribsInfo {
pairs: &cell.attribs, pairs: &cell.attribs,
parent_id: id_buttons, parent_id: id_buttons,
@@ -121,7 +135,7 @@ impl ContextMenu {
}); });
} }
if idx < blueprint.cells.len() - 1 { if idx < cells.len() - 1 {
inner_parser.parse_template(&doc_params, "Separator", layout, id_buttons, Default::default())?; inner_parser.parse_template(&doc_params, "Separator", layout, id_buttons, Default::default())?;
} }
} }
@@ -129,8 +143,8 @@ impl ContextMenu {
} }
pub fn tick(&mut self, layout: &mut Layout, parser_state: &mut ParserState) -> anyhow::Result<TickResult> { pub fn tick(&mut self, layout: &mut Layout, parser_state: &mut ParserState) -> anyhow::Result<TickResult> {
if let Some(mut p) = self.pending_open.take() { if let Some(p) = self.pending_open.take() {
self.open_process(&mut p, layout, parser_state)?; self.open_process(p, layout, parser_state)?;
let _ = self.tasks.drain(); let _ = self.tasks.drain();
return Ok(TickResult::Opened); return Ok(TickResult::Opened);
} }

View File

@@ -14,7 +14,9 @@ idmap-derive.workspace = true
log.workspace = true log.workspace = true
serde = { workspace = true, features = ["rc"] } serde = { workspace = true, features = ["rc"] }
serde_json.workspace = true serde_json.workspace = true
strum.workspace = true
xdg.workspace = true xdg.workspace = true
chrono = "0.4.42" chrono = "0.4.42"
smol = "2.0.2" smol = "2.0.2"
wgui = { path = "../wgui/" } wgui = { path = "../wgui/" }

View File

@@ -3,6 +3,7 @@ use std::{collections::HashMap, sync::Arc};
use chrono::Offset; use chrono::Offset;
use idmap::IdMap; use idmap::IdMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{AsRefStr, EnumProperty, EnumString, VariantArray};
use crate::{ use crate::{
astr_containers::{AStrMap, AStrSet}, astr_containers::{AStrMap, AStrSet},
@@ -13,6 +14,22 @@ use crate::{
pub type PwTokenMap = AStrMap<String>; pub type PwTokenMap = AStrMap<String>;
pub type SerializedWindowStates = HashMap<Arc<str>, OverlayWindowState>; pub type SerializedWindowStates = HashMap<Arc<str>, OverlayWindowState>;
#[derive(Default, Clone, Copy, Serialize, Deserialize, AsRefStr, EnumString, EnumProperty, VariantArray)]
pub enum CaptureMethod {
#[default]
#[serde(alias = "pipewire", alias = "auto")]
#[strum(props(Text = "PipeWire GPU", Tooltip = "APP_SETTINGS.OPTION.PIPEWIRE_HELP"))]
PipeWire,
#[serde(alias = "pw-fallback")]
#[strum(props(Text = "PipeWire CPU", Tooltip = "APP_SETTINGS.OPTION.PW_FALLBACK_HELP"))]
PwFallback,
#[serde(alias = "screencopy")]
#[strum(props(Text = "ScreenCopy CPU", Tooltip = "APP_SETTINGS.OPTION.SCREENCOPY_HELP"))]
ScreenCopy,
}
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct SerializedWindowSet { pub struct SerializedWindowSet {
pub name: Arc<str>, pub name: Arc<str>,
@@ -76,10 +93,6 @@ fn def_timezones() -> Vec<String> {
} }
} }
fn def_auto() -> Arc<str> {
"auto".into()
}
fn def_empty() -> Arc<str> { fn def_empty() -> Arc<str> {
"".into() "".into()
} }
@@ -174,8 +187,8 @@ pub struct GeneralConfig {
#[serde(default)] #[serde(default)]
pub custom_panels: AStrSet, pub custom_panels: AStrSet,
#[serde(default = "def_auto")] #[serde(default)]
pub capture_method: Arc<str>, pub capture_method: CaptureMethod,
#[serde(default = "def_point7")] #[serde(default = "def_point7")]
pub xr_click_sensitivity: f32, pub xr_click_sensitivity: f32,

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use wlx_common::{ use wlx_common::{
astr_containers::AStrMap, astr_containers::AStrMap,
config::{GeneralConfig, SerializedWindowSet, SerializedWindowStates}, config::{CaptureMethod, GeneralConfig, SerializedWindowSet, SerializedWindowStates},
config_io, config_io,
overlays::BackendAttribValue, overlays::BackendAttribValue,
}; };
@@ -138,6 +138,7 @@ pub struct AutoSettings {
pub opaque_background: bool, pub opaque_background: bool,
pub xwayland_by_default: bool, pub xwayland_by_default: bool,
pub context_menu_hold_and_release: bool, pub context_menu_hold_and_release: bool,
pub capture_method: CaptureMethod,
} }
fn get_settings_path() -> PathBuf { fn get_settings_path() -> PathBuf {
@@ -181,6 +182,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
opaque_background: config.opaque_background, opaque_background: config.opaque_background,
xwayland_by_default: config.xwayland_by_default, xwayland_by_default: config.xwayland_by_default,
context_menu_hold_and_release: config.context_menu_hold_and_release, context_menu_hold_and_release: config.context_menu_hold_and_release,
capture_method: config.capture_method,
}; };
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic

View File

@@ -20,7 +20,7 @@ use wgui::{
parser::{self, AttribPair, CustomAttribsInfoOwned, Fetchable, ParserState}, parser::{self, AttribPair, CustomAttribsInfoOwned, Fetchable, ParserState},
taffy, taffy,
widget::EventResult, widget::EventResult,
windowing::context_menu::{ContextMenu, OpenParams}, windowing::context_menu::{Blueprint, ContextMenu, OpenParams},
}; };
use wlx_common::overlays::ToastTopic; use wlx_common::overlays::ToastTopic;
@@ -207,8 +207,9 @@ pub(super) fn setup_custom_button<S: 'static>(
let callback: EventCallback<AppState, S> = match command { let callback: EventCallback<AppState, S> = match command {
"::ContextMenuOpen" => { "::ContextMenuOpen" => {
let Some(template_name) = args.next() else { let Some(template_name) = args.next() else {
log::warn!( log::error!(
"{command} has incorrect arguments. Should be: {command} <context_menu>" "{:?}: {command} has invalid arguments",
parser_state.path.get_path_buf()
); );
return; return;
}; };
@@ -230,8 +231,10 @@ pub(super) fn setup_custom_button<S: 'static>(
move |_common, data, _app, _| { move |_common, data, _app, _| {
context_menu.borrow_mut().open(OpenParams { context_menu.borrow_mut().open(OpenParams {
on_custom_attribs: Some(on_custom_attribs.clone()), on_custom_attribs: Some(on_custom_attribs.clone()),
blueprint: Blueprint::Template {
template_name: template_name.clone(), template_name: template_name.clone(),
template_params: template_params.clone(), template_params: template_params.clone(),
},
position: data.metadata.get_mouse_pos_absolute().unwrap(), //want panic position: data.metadata.get_mouse_pos_absolute().unwrap(), //want panic
}); });
Ok(EventResult::Consumed) Ok(EventResult::Consumed)
@@ -249,14 +252,18 @@ pub(super) fn setup_custom_button<S: 'static>(
} }
"::ElementSetDisplay" => { "::ElementSetDisplay" => {
let (Some(id), Some(value)) = (args.next(), args.next()) else { let (Some(id), Some(value)) = (args.next(), args.next()) else {
log::warn!( log::error!(
"{command} has incorrect arguments. Should be: {command} <element_id> <display>" "{:?}: {command} has invalid arguments",
parser_state.path.get_path_buf()
); );
return; return;
}; };
let Ok(widget_id) = parser_state.data.get_widget_id(id) else { let Ok(widget_id) = parser_state.data.get_widget_id(id) else {
log::warn!("{command}: no element exists with ID '{id}'"); log::warn!(
"{:?}: {command}: no element exists with ID '{id}'",
parser_state.path.get_path_buf()
);
return; return;
}; };
@@ -290,7 +297,10 @@ pub(super) fn setup_custom_button<S: 'static>(
"::SetToggle" => { "::SetToggle" => {
let arg = args.next().unwrap_or_default(); let arg = args.next().unwrap_or_default();
let Ok(set_idx) = arg.parse() else { let Ok(set_idx) = arg.parse() else {
log::error!("{command} has invalid argument: \"{arg}\""); log::error!(
"{:?}: {command} has invalid argument: \"{arg}\"",
parser_state.path.get_path_buf()
);
return; return;
}; };
Box::new(move |_common, data, app, _| { Box::new(move |_common, data, app, _| {
@@ -306,7 +316,10 @@ pub(super) fn setup_custom_button<S: 'static>(
"::SetSwitch" => { "::SetSwitch" => {
let arg = args.next().unwrap_or_default(); let arg = args.next().unwrap_or_default();
let Ok(set_idx) = arg.parse::<i32>() else { let Ok(set_idx) = arg.parse::<i32>() else {
log::error!("{command} has invalid argument: \"{arg}\""); log::error!(
"{:?}: {command} has invalid argument: \"{arg}\"",
parser_state.path.get_path_buf()
);
return; return;
}; };
let maybe_set = if set_idx < 0 { let maybe_set = if set_idx < 0 {
@@ -327,7 +340,10 @@ pub(super) fn setup_custom_button<S: 'static>(
"::OverlayReset" => { "::OverlayReset" => {
let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into(); let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into();
if arg.len() < 1 { if arg.len() < 1 {
log::error!("{command} has missing arguments"); log::error!(
"{:?}: {command} has missing arguments",
parser_state.path.get_path_buf()
);
return; return;
}; };
@@ -346,7 +362,10 @@ pub(super) fn setup_custom_button<S: 'static>(
"::OverlayToggle" => { "::OverlayToggle" => {
let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into(); let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into();
if arg.len() < 1 { if arg.len() < 1 {
log::error!("{command} has missing arguments"); log::error!(
"{:?}: {command} has missing arguments",
parser_state.path.get_path_buf()
);
return; return;
}; };
@@ -366,7 +385,10 @@ pub(super) fn setup_custom_button<S: 'static>(
"::OverlayDrop" => { "::OverlayDrop" => {
let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into(); let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into();
if arg.len() < 1 { if arg.len() < 1 {
log::error!("{command} has missing arguments"); log::error!(
"{:?}: {command} has missing arguments",
parser_state.path.get_path_buf()
);
return; return;
}; };
@@ -402,7 +424,10 @@ pub(super) fn setup_custom_button<S: 'static>(
"::CustomOverlayReload" => { "::CustomOverlayReload" => {
let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into(); let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into();
if arg.len() < 1 { if arg.len() < 1 {
log::error!("{command} has missing arguments"); log::error!(
"{:?}: {command} has missing arguments",
parser_state.path.get_path_buf()
);
return; return;
}; };
@@ -440,7 +465,10 @@ pub(super) fn setup_custom_button<S: 'static>(
"::WvrOverlayCloseWindow" => { "::WvrOverlayCloseWindow" => {
let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into(); let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into();
if arg.len() < 1 { if arg.len() < 1 {
log::error!("{command} has missing arguments"); log::error!(
"{:?}: {command} has missing arguments",
parser_state.path.get_path_buf()
);
return; return;
}; };
Box::new(move |_common, data, app, _| { Box::new(move |_common, data, app, _| {
@@ -463,7 +491,10 @@ pub(super) fn setup_custom_button<S: 'static>(
"::WvrOverlayKillProcess" | "::WvrOverlayTermProcess" => { "::WvrOverlayKillProcess" | "::WvrOverlayTermProcess" => {
let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into(); let arg: Arc<str> = args.collect::<Vec<_>>().join(" ").into();
if arg.len() < 1 { if arg.len() < 1 {
log::error!("{command} has missing arguments"); log::error!(
"{:?}: {command} has missing arguments",
parser_state.path.get_path_buf()
);
return; return;
}; };
@@ -585,7 +616,10 @@ pub(super) fn setup_custom_button<S: 'static>(
}), }),
"::SendKey" => { "::SendKey" => {
let Some(key) = args.next().and_then(|s| VirtualKey::from_str(s).ok()) else { let Some(key) = args.next().and_then(|s| VirtualKey::from_str(s).ok()) else {
log::error!("{command} has bad/missing arguments"); log::error!(
"{:?}: {command} has bad/missing arguments",
parser_state.path.get_path_buf()
);
return; return;
}; };
let Some(down) = args.next().and_then(|s| match s.to_lowercase().as_str() { let Some(down) = args.next().and_then(|s| match s.to_lowercase().as_str() {
@@ -593,7 +627,10 @@ pub(super) fn setup_custom_button<S: 'static>(
"up" => Some(false), "up" => Some(false),
_ => None, _ => None,
}) else { }) else {
log::error!("{command} has bad/missing arguments"); log::error!(
"{:?}: {command} has bad/missing arguments",
parser_state.path.get_path_buf()
);
return; return;
}; };
Box::new(move |_common, data, app, _| { Box::new(move |_common, data, app, _| {
@@ -640,7 +677,10 @@ pub(super) fn setup_custom_button<S: 'static>(
use crate::subsystem::osc::parse_osc_value; use crate::subsystem::osc::parse_osc_value;
let Some(address) = args.next().map(std::string::ToString::to_string) else { let Some(address) = args.next().map(std::string::ToString::to_string) else {
log::error!("{command} has missing arguments"); log::error!(
"{:?}: {command} has bad/missing arguments",
parser_state.path.get_path_buf()
);
return; return;
}; };

View File

@@ -1,4 +1,4 @@
use std::{cell::RefCell, rc::Rc}; use std::{any, cell::RefCell, rc::Rc};
use button::setup_custom_button; use button::setup_custom_button;
use glam::{Affine2, Vec2, vec2}; use glam::{Affine2, Vec2, vec2};

View File

@@ -18,7 +18,10 @@ use crate::{
FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, ui_transform, FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, ui_transform,
}, },
}; };
use wlx_common::overlays::{BackendAttrib, BackendAttribValue, MouseTransform, StereoMode}; use wlx_common::{
config::CaptureMethod,
overlays::{BackendAttrib, BackendAttribValue, MouseTransform, StereoMode},
};
use super::capture::{ScreenPipeline, WlxCaptureIn, WlxCaptureOut, receive_callback}; use super::capture::{ScreenPipeline, WlxCaptureIn, WlxCaptureOut, receive_callback};
@@ -144,10 +147,12 @@ impl OverlayBackend for ScreenBackend {
.ext_external_memory_dma_buf .ext_external_memory_dma_buf
&& self.capture.supports_dmbuf(); && self.capture.supports_dmbuf();
let allow_dmabuf = &*app.session.config.capture_method != "pw_fallback" let capture_method = app.session.config.capture_method;
&& &*app.session.config.capture_method != "screencopy";
let capture_method = app.session.config.capture_method.clone(); let allow_dmabuf = !matches!(
capture_method,
CaptureMethod::PwFallback | CaptureMethod::ScreenCopy
);
let dmabuf_formats = if !supports_dmabuf { let dmabuf_formats = if !supports_dmabuf {
log::info!("Capture method does not support DMA-buf"); log::info!("Capture method does not support DMA-buf");
@@ -158,7 +163,10 @@ impl OverlayBackend for ScreenBackend {
} }
&Vec::new() &Vec::new()
} else if !allow_dmabuf { } else if !allow_dmabuf {
log::info!("Not using DMA-buf capture due to {capture_method}"); log::info!(
"Not using DMA-buf capture due to {}",
capture_method.as_ref()
);
if app.gfx_extras.queue_capture.is_none() { if app.gfx_extras.queue_capture.is_none() {
log::warn!( log::warn!(
"Current GPU does not support multiple queues. Software capture will take place on the main thread. Expect degraded performance." "Current GPU does not support multiple queues. Software capture will take place on the main thread. Expect degraded performance."

View File

@@ -6,7 +6,10 @@ use wlx_capture::{
wlr_dmabuf::WlrDmabufCapture, wlr_dmabuf::WlrDmabufCapture,
wlr_screencopy::WlrScreencopyCapture, wlr_screencopy::WlrScreencopyCapture,
}; };
use wlx_common::{astr_containers::AStrMapExt, config::PwTokenMap}; use wlx_common::{
astr_containers::AStrMapExt,
config::{CaptureMethod, PwTokenMap},
};
use crate::{ use crate::{
overlays::screen::create_screen_from_backend, overlays::screen::create_screen_from_backend,
@@ -43,18 +46,14 @@ impl ScreenBackend {
#[allow(clippy::useless_let_if_seq)] #[allow(clippy::useless_let_if_seq)]
pub fn create_screen_renderer_wl( pub fn create_screen_renderer_wl(
output: &WlxOutput, output: &WlxOutput,
has_wlr_dmabuf: bool,
has_wlr_screencopy: bool, has_wlr_screencopy: bool,
pw_token_store: &mut PwTokenMap, pw_token_store: &mut PwTokenMap,
app: &mut AppState, app: &mut AppState,
) -> Option<ScreenBackend> { ) -> Option<ScreenBackend> {
let mut capture: Option<ScreenBackend> = None; let mut capture: Option<ScreenBackend> = None;
if (&*app.session.config.capture_method == "wlr-dmabuf") && has_wlr_dmabuf {
log::info!("{}: Using Wlr DMA-Buf", &output.name);
capture = ScreenBackend::new_wlr_dmabuf(output, app);
}
if &*app.session.config.capture_method == "screencopy" && has_wlr_screencopy { if matches!(app.session.config.capture_method, CaptureMethod::ScreenCopy) && has_wlr_screencopy
{
log::info!("{}: Using Wlr Screencopy Wl-SHM", &output.name); log::info!("{}: Using Wlr Screencopy Wl-SHM", &output.name);
capture = ScreenBackend::new_wlr_screencopy(output, app); capture = ScreenBackend::new_wlr_screencopy(output, app);
} }
@@ -102,7 +101,6 @@ pub fn create_screens_wayland(wl: &mut WlxClient, app: &mut AppState) -> ScreenC
let mut pw_tokens: PwTokenMap = load_pw_token_config().unwrap_or_default(); let mut pw_tokens: PwTokenMap = load_pw_token_config().unwrap_or_default();
let pw_tokens_copy = pw_tokens.clone(); let pw_tokens_copy = pw_tokens.clone();
let has_wlr_dmabuf = wl.maybe_wlr_dmabuf_mgr.is_some();
let has_wlr_screencopy = wl.maybe_wlr_screencopy_mgr.is_some(); let has_wlr_screencopy = wl.maybe_wlr_screencopy_mgr.is_some();
for (id, output) in &wl.outputs { for (id, output) in &wl.outputs {
@@ -118,13 +116,9 @@ pub fn create_screens_wayland(wl: &mut WlxClient, app: &mut AppState) -> ScreenC
output.logical_pos, output.logical_pos,
); );
if let Some(mut backend) = create_screen_renderer_wl( if let Some(mut backend) =
output, create_screen_renderer_wl(output, has_wlr_screencopy, &mut pw_tokens, app)
has_wlr_dmabuf, {
has_wlr_screencopy,
&mut pw_tokens,
app,
) {
backend.logical_pos = vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32); backend.logical_pos = vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32);
backend.logical_size = vec2(output.logical_size.0 as f32, output.logical_size.1 as f32); backend.logical_size = vec2(output.logical_size.0 as f32, output.logical_size.1 as f32);
backend.mouse_transform_original = output.transform; backend.mouse_transform_original = output.transform;