sorry about monster commit
This commit is contained in:
29
Cargo.lock
generated
29
Cargo.lock
generated
@@ -659,9 +659,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.0"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cexpr",
|
||||
@@ -1130,7 +1130,7 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6"
|
||||
dependencies = [
|
||||
"bindgen 0.72.0",
|
||||
"bindgen 0.72.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2740,9 +2740,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
|
||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -6213,23 +6213,6 @@ dependencies = [
|
||||
"wayland-protocols",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wlx-capture"
|
||||
version = "0.5.3"
|
||||
source = "git+https://github.com/galister/wlx-capture?tag=v0.5.3#4479bd4bdd2b570aec9692e55b513ec7c0a17e7f"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"drm-fourcc",
|
||||
"idmap",
|
||||
"libc",
|
||||
"log",
|
||||
"pipewire",
|
||||
"rxscreen",
|
||||
"smithay-client-toolkit",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wlx-overlay-s"
|
||||
version = "25.4.2"
|
||||
@@ -6283,7 +6266,7 @@ dependencies = [
|
||||
"wayvr_ipc",
|
||||
"wgui",
|
||||
"winit",
|
||||
"wlx-capture 0.5.3 (git+https://github.com/galister/wlx-capture?tag=v0.5.3)",
|
||||
"wlx-capture",
|
||||
"xcb",
|
||||
"xdg 3.0.0",
|
||||
"xkbcommon 0.8.0",
|
||||
|
||||
@@ -766,6 +766,45 @@ impl CustomAttribsInfo<'_> {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn to_owned(&self) -> CustomAttribsInfoOwned {
|
||||
CustomAttribsInfoOwned {
|
||||
parent_id: self.parent_id,
|
||||
widget_id: self.widget_id,
|
||||
pairs: self
|
||||
.pairs
|
||||
.iter()
|
||||
.map(|p| CustomAttribPairOwned {
|
||||
attrib: p.attrib.to_string(),
|
||||
value: p.value.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CustomAttribPairOwned {
|
||||
pub attrib: String, // without _ at the beginning
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub struct CustomAttribsInfoOwned {
|
||||
pub parent_id: WidgetID,
|
||||
pub widget_id: WidgetID,
|
||||
pub pairs: Vec<CustomAttribPairOwned>,
|
||||
}
|
||||
|
||||
impl CustomAttribsInfoOwned {
|
||||
pub fn get_value(&self, attrib_name: &str) -> Option<&str> {
|
||||
// O(n) search, these pairs won't be problematically big anyways
|
||||
for pair in self.pairs.iter() {
|
||||
if pair.attrib == attrib_name {
|
||||
return Some(pair.value.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub type OnCustomAttribsFunc = Box<dyn Fn(CustomAttribsInfo)>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use cosmic_text::{Attrs, Buffer, Metrics, Shaping, Wrap};
|
||||
use cosmic_text::{Attrs, AttrsList, Buffer, Metrics, Shaping, Wrap};
|
||||
use slotmap::Key;
|
||||
use taffy::AvailableSpace;
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
globals::Globals,
|
||||
i18n::{I18n, Translation},
|
||||
layout::WidgetID,
|
||||
renderer_vk::text::{FONT_SYSTEM, TextStyle},
|
||||
renderer_vk::text::{TextStyle, FONT_SYSTEM},
|
||||
};
|
||||
|
||||
use super::{WidgetObj, WidgetState};
|
||||
@@ -84,12 +84,27 @@ impl WidgetLabel {
|
||||
true
|
||||
}
|
||||
|
||||
fn update_attrs(&mut self) {
|
||||
let attrs = Attrs::from(&self.params.style);
|
||||
for line in self.buffer.borrow_mut().lines.iter_mut() {
|
||||
line.set_attrs_list(AttrsList::new(&attrs));
|
||||
}
|
||||
}
|
||||
|
||||
// set text and check if it needs to be re-rendered/re-layouted
|
||||
pub fn set_text(&mut self, common: &mut CallbackDataCommon, translation: Translation) {
|
||||
if self.set_text_simple(&mut common.i18n(), translation) {
|
||||
common.mark_widget_dirty(self.id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_color(&mut self, common: &mut CallbackDataCommon, color: drawing::Color, apply_to_existing_text: bool) {
|
||||
self.params.style.color = Some(color);
|
||||
if apply_to_existing_text {
|
||||
self.update_attrs();
|
||||
common.mark_widget_dirty(self.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetObj for WidgetLabel {
|
||||
|
||||
@@ -59,7 +59,7 @@ pub enum WlxFrame {
|
||||
MemPtr(MemPtrFrame),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub enum Transform {
|
||||
#[default]
|
||||
Undefined,
|
||||
|
||||
@@ -19,17 +19,19 @@ use smithay_client_toolkit::reexports::{
|
||||
|
||||
pub use wayland_client;
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
|
||||
backend::WaylandError,
|
||||
globals::{GlobalList, GlobalListContents, registry_queue_init},
|
||||
globals::{registry_queue_init, GlobalList, GlobalListContents},
|
||||
protocol::{
|
||||
wl_output::{self, Transform, WlOutput},
|
||||
wl_registry::{self, WlRegistry},
|
||||
wl_seat::WlSeat,
|
||||
wl_shm::WlShm,
|
||||
},
|
||||
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
|
||||
};
|
||||
|
||||
use crate::frame;
|
||||
|
||||
pub enum OutputChangeEvent {
|
||||
/// New output has been created.
|
||||
Create(u32),
|
||||
@@ -50,7 +52,7 @@ pub struct WlxOutput {
|
||||
pub size: (i32, i32),
|
||||
pub logical_pos: (i32, i32),
|
||||
pub logical_size: (i32, i32),
|
||||
pub transform: Transform,
|
||||
pub transform: frame::Transform,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
@@ -125,7 +127,7 @@ impl WlxClient {
|
||||
size: (0, 0),
|
||||
logical_pos: (0, 0),
|
||||
logical_size: (0, 0),
|
||||
transform: Transform::Normal,
|
||||
transform: frame::Transform::Normal,
|
||||
done: false,
|
||||
};
|
||||
|
||||
@@ -192,17 +194,17 @@ impl WlxClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wl_transform_to_frame_transform(transform: Transform) -> crate::frame::Transform {
|
||||
fn wl_transform_to_frame_transform(transform: Transform) -> frame::Transform {
|
||||
match transform {
|
||||
Transform::Normal => crate::frame::Transform::Normal,
|
||||
Transform::_90 => crate::frame::Transform::Rotated90,
|
||||
Transform::_180 => crate::frame::Transform::Rotated180,
|
||||
Transform::_270 => crate::frame::Transform::Rotated270,
|
||||
Transform::Flipped => crate::frame::Transform::Flipped,
|
||||
Transform::Flipped90 => crate::frame::Transform::Flipped90,
|
||||
Transform::Flipped180 => crate::frame::Transform::Flipped180,
|
||||
Transform::Flipped270 => crate::frame::Transform::Flipped270,
|
||||
_ => crate::frame::Transform::Undefined,
|
||||
Transform::Normal => frame::Transform::Normal,
|
||||
Transform::_90 => frame::Transform::Rotated90,
|
||||
Transform::_180 => frame::Transform::Rotated180,
|
||||
Transform::_270 => frame::Transform::Rotated270,
|
||||
Transform::Flipped => frame::Transform::Flipped,
|
||||
Transform::Flipped90 => frame::Transform::Flipped90,
|
||||
Transform::Flipped180 => frame::Transform::Flipped180,
|
||||
Transform::Flipped270 => frame::Transform::Flipped270,
|
||||
_ => frame::Transform::Undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,8 +329,8 @@ impl Dispatch<WlOutput, u32> for WlxClient {
|
||||
if let Some(output) = state.outputs.get_mut(*data) {
|
||||
let transform = transform.into_result().unwrap_or(Transform::Normal);
|
||||
let old_transform = output.transform;
|
||||
output.transform = transform;
|
||||
if output.done && old_transform != transform {
|
||||
output.transform = wl_transform_to_frame_transform(transform);
|
||||
if output.done && old_transform != output.transform {
|
||||
log::info!(
|
||||
"{}: Transform changed {:?} -> {:?}",
|
||||
output.name,
|
||||
|
||||
@@ -11,7 +11,7 @@ use wayland_client::{Connection, QueueHandle, Dispatch, Proxy};
|
||||
|
||||
use crate::{
|
||||
frame::{DmabufFrame, DrmFormat, FramePlane, WlxFrame},
|
||||
wayland::{wl_transform_to_frame_transform, WlxClient},
|
||||
wayland::WlxClient,
|
||||
WlxCapture,
|
||||
};
|
||||
|
||||
@@ -146,7 +146,7 @@ fn request_dmabuf_frame(
|
||||
return client;
|
||||
};
|
||||
|
||||
let transform = wl_transform_to_frame_transform(output.transform);
|
||||
let transform = output.transform;
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel::<zwlr_export_dmabuf_frame_v1::Event>(16);
|
||||
let name = output.name.clone();
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
DrmFormat, FourCC, FrameFormat, FramePlane, MemFdFrame, WlxFrame, DRM_FORMAT_ARGB8888,
|
||||
DRM_FORMAT_XRGB8888,
|
||||
},
|
||||
wayland::{wl_transform_to_frame_transform, WlxClient},
|
||||
wayland::WlxClient,
|
||||
WlxCapture,
|
||||
};
|
||||
|
||||
@@ -194,7 +194,7 @@ where
|
||||
return client;
|
||||
};
|
||||
|
||||
let transform = wl_transform_to_frame_transform(output.transform);
|
||||
let transform = output.transform;
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel::<ScreenCopyEvent>(16);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ smallvec = "1.13.2"
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
sysinfo = { version = "0.35" }
|
||||
thiserror = "2.0"
|
||||
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.5.3", default-features = false }
|
||||
wlx-capture = { path = "../wlx-capture" }
|
||||
libmonado = { version = "1.3.2", optional = true }
|
||||
winit = { version = "0.30", optional = true }
|
||||
xdg = "3.0"
|
||||
|
||||
@@ -14,22 +14,26 @@
|
||||
<var key="clock_alt_tz_size" value="14" />
|
||||
</theme>
|
||||
|
||||
<macro name="button_style"
|
||||
margin="2" overflow="hidden" box_sizing="border_box" align_items="center" justify_content="center"
|
||||
border_color="#0044CC" border="2" round="8" color="#000A1C" color2="#000002" gradient="vertical" />
|
||||
|
||||
<template name="Device">
|
||||
<sprite color="~device_color" width="${size}" height="${size}" src="${src}" />
|
||||
</template>
|
||||
|
||||
<template name="Set">
|
||||
<div align_items="center" justify_content="center" flex_wrap="wrap" align_content="center">
|
||||
<Button macro="button_style" _press="::OverlayToggle ${handle}">
|
||||
<sprite width="40" height="40" color="~set_color" src="watch/set2.svg" />
|
||||
<div position="absolute" margin_top="11" >
|
||||
<label text="${name}" size="24" color="#000000" weight="bold" />
|
||||
<label text="${display}" size="24" color="#000000" weight="bold" />
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<elements>
|
||||
<div width="400" height="200">
|
||||
<rectangle width="100%" height="100%" padding="4" box_sizing="content_box" flex_wrap="wrap" gap="16" color="~bg_color">
|
||||
<rectangle width="100%" height="100%" padding="4" box_sizing="content_box" flex_wrap="wrap" flex_direction="column" gap="4" color="~bg_color">
|
||||
<div width="100%" flex_direction="row">
|
||||
<Device src="watch/hmd.svg" size="40" />
|
||||
<Device src="watch/controller_l.svg" size="36" />
|
||||
@@ -39,38 +43,33 @@
|
||||
<Device src="watch/track3.svg" size="40" />
|
||||
</div>
|
||||
<div flex_direction="row">
|
||||
<div id="clock_main" flex_direction="column" padding="4">
|
||||
<label text="23:59" id="clock0_time" color="~clock0_color" size="~clock0_size" weight="bold" />
|
||||
<label text="22/2/2022" id="clock0_date" color="~clock0_color" size="~clock0_date_size" weight="bold" />
|
||||
<div flex_direction="column" padding="4">
|
||||
<label text="23:59" _source="clock" _display="time" color="~clock0_color" size="~clock0_size" weight="bold" />
|
||||
<label text="22/2/2022" _source="clock" _display="date" color="~clock0_color" size="~clock0_date_size" weight="bold" />
|
||||
<div width="100%" padding="2" />
|
||||
<label text="Friday" id="clock0_dow" color="~clock0_color" size="~clock0_dow_size" weight="bold" />
|
||||
<label text="Tuesday" _source="clock" _display="dow" color="~clock0_color" size="~clock0_dow_size" weight="bold" />
|
||||
</div>
|
||||
<div width="10" height="100%" />
|
||||
<div id="clock_alt" flex_direction="column" padding="4">
|
||||
<div flex_direction="column" padding="4">
|
||||
<!-- Timezone names here are only placeholders. Set your timezones via ~/.config/wlxoverlay/conf.d -->
|
||||
<div width="100%" padding="2" />
|
||||
<label text="Paris" id="clock1_tz" color="~clock_alt1_color" size="~clock_alt_tz_size" weight="bold" />
|
||||
<label text="23:59" id="clock1_time" color="~clock_alt1_color" size="~clock_alt_size" weight="bold" />
|
||||
<label text="Paris" _source="clock" _display="name" _timezone="0" color="~clock_alt1_color" size="~clock_alt_tz_size" weight="bold" />
|
||||
<label text="23:59" _source="clock" _display="time" _timezone="0" color="~clock_alt1_color" size="~clock_alt_size" weight="bold" />
|
||||
<div width="100%" padding="2" />
|
||||
<label text="Chicago" id="clock2_tz" color="~clock_alt2_color" size="~clock_alt_tz_size" weight="bold" />
|
||||
<label text="23:59" id="clock2_time" color="~clock_alt2_color" size="~clock_alt_size" weight="bold" />
|
||||
<label text="New York" _source="clock" _display="name" _timezone="1" color="~clock_alt2_color" size="~clock_alt_tz_size" weight="bold" />
|
||||
<label text="23:59" _source="clock" _display="time" _timezone="1" color="~clock_alt2_color" size="~clock_alt_size" weight="bold" />
|
||||
</div>
|
||||
</div>
|
||||
<div width="100%" flex_direction="row">
|
||||
<div id="btn_home">
|
||||
<Button macro="button_style" _press="::DashToggle">
|
||||
<sprite color="~set_color" width="40" height="40" src="watch/home.svg" />
|
||||
</div>
|
||||
<div id="sets" ignore_in_mode="dev">
|
||||
</Button>
|
||||
<div id="sets">
|
||||
<!-- Will populate <Set> tags at runtime -->
|
||||
</div>
|
||||
<div ignore_in_mode="live">
|
||||
<!-- Example sets for testing -->
|
||||
<Set name="A" />
|
||||
<Set name="B" />
|
||||
<Set name="C" />
|
||||
</div>
|
||||
<div id="btn_edit">
|
||||
<Button macro="button_style" _press="::EditToggle">
|
||||
<sprite color="~set_color" width="40" height="40" src="watch/edit.svg" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</rectangle>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,9 @@ use crate::{
|
||||
config::AStrSetExt,
|
||||
overlays::{
|
||||
anchor::create_anchor,
|
||||
keyboard::{KEYBOARD_NAME, builder::create_keyboard},
|
||||
keyboard::{builder::create_keyboard, KEYBOARD_NAME},
|
||||
screen::create_screens,
|
||||
watch::{WATCH_NAME, create_watch},
|
||||
watch::{create_watch, WATCH_NAME},
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -61,19 +61,19 @@ where
|
||||
if let Some((_, s, _)) = data.screens.first() {
|
||||
show_screens.arc_set(s.name.clone());
|
||||
}
|
||||
for (meta, mut state, backend) in data.screens {
|
||||
if show_screens.arc_get(state.name.as_ref()) {
|
||||
state.show_hide = true;
|
||||
}
|
||||
overlays.insert(
|
||||
state.id.0,
|
||||
OverlayData::<T> {
|
||||
state,
|
||||
..OverlayData::from_backend(backend)
|
||||
},
|
||||
);
|
||||
app.screens.push(meta);
|
||||
}
|
||||
for (meta, mut state, backend) in data.screens {
|
||||
if show_screens.arc_get(state.name.as_ref()) {
|
||||
state.show_hide = true;
|
||||
}
|
||||
overlays.insert(
|
||||
state.id.0,
|
||||
OverlayData::<T> {
|
||||
state,
|
||||
..OverlayData::from_backend(backend)
|
||||
},
|
||||
);
|
||||
app.screens.push(meta);
|
||||
}
|
||||
|
||||
maybe_keymap = keymap;
|
||||
|
||||
@@ -11,5 +11,6 @@ pub mod openxr;
|
||||
pub mod wayvr;
|
||||
|
||||
pub mod overlay;
|
||||
pub mod set;
|
||||
|
||||
pub mod task;
|
||||
|
||||
@@ -2,18 +2,18 @@ use std::{
|
||||
collections::VecDeque,
|
||||
ops::Add,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{anyhow, Result};
|
||||
use ovr_overlay::{
|
||||
TrackedDeviceIndex,
|
||||
sys::{ETrackedDeviceProperty, EVRApplicationType, EVREventType},
|
||||
TrackedDeviceIndex,
|
||||
};
|
||||
use vulkano::{Handle, VulkanObject, device::physical::PhysicalDevice};
|
||||
use vulkano::{device::physical::PhysicalDevice, Handle, VulkanObject};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
input::interact,
|
||||
openvr::{
|
||||
helpers::adjust_gain,
|
||||
input::{OpenVrInputSource, set_action_manifest},
|
||||
input::{set_action_manifest, OpenVrInputSource},
|
||||
lines::LinePool,
|
||||
manifest::{install_manifest, uninstall_manifest},
|
||||
overlay::OpenVrOverlayData,
|
||||
@@ -29,10 +29,10 @@ use crate::{
|
||||
overlay::{OverlayData, ShouldRender},
|
||||
task::{SystemTask, TaskType},
|
||||
},
|
||||
graphics::{CommandBuffers, init_openvr_graphics},
|
||||
graphics::{init_openvr_graphics, CommandBuffers},
|
||||
overlays::{
|
||||
toast::{Toast, ToastTopic},
|
||||
watch::{WATCH_NAME, watch_fade},
|
||||
watch::{watch_fade, WATCH_NAME},
|
||||
},
|
||||
state::AppState,
|
||||
subsystem::notifications::NotificationManager,
|
||||
@@ -110,8 +110,8 @@ pub fn openvr_run(
|
||||
TrackedDeviceIndex::HMD,
|
||||
ETrackedDeviceProperty::Prop_UserIpdMeters_Float,
|
||||
) {
|
||||
state.input_state.ipd = (ipd * 10000.0).round() * 0.1;
|
||||
log::info!("IPD: {:.1} mm", state.input_state.ipd);
|
||||
state.input_state.ipd = (ipd * 1000.0).round();
|
||||
log::info!("IPD: {:.0} mm", state.input_state.ipd);
|
||||
}
|
||||
|
||||
let _ = install_manifest(&mut app_mgr);
|
||||
@@ -182,7 +182,7 @@ pub fn openvr_run(
|
||||
TrackedDeviceIndex::HMD,
|
||||
ETrackedDeviceProperty::Prop_UserIpdMeters_Float,
|
||||
) {
|
||||
let ipd = (ipd * 10000.0).round() * 0.1;
|
||||
let ipd = (ipd * 1000.0).round();
|
||||
if (ipd - state.input_state.ipd).abs() > 0.05 {
|
||||
log::info!("IPD: {:.1} mm -> {:.1} mm", state.input_state.ipd, ipd);
|
||||
Toast::new(ToastTopic::IpdChange, "IPD".into(), format!("{ipd:.1} mm"))
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::{
|
||||
collections::VecDeque,
|
||||
ops::Add,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -23,10 +23,10 @@ use crate::{
|
||||
overlay::{OverlayData, ShouldRender},
|
||||
task::{SystemTask, TaskType},
|
||||
},
|
||||
graphics::{CommandBuffers, init_openxr_graphics},
|
||||
graphics::{init_openxr_graphics, CommandBuffers},
|
||||
overlays::{
|
||||
toast::{Toast, ToastTopic},
|
||||
watch::{WATCH_NAME, watch_fade},
|
||||
watch::{watch_fade, WATCH_NAME},
|
||||
},
|
||||
state::AppState,
|
||||
subsystem::notifications::NotificationManager,
|
||||
@@ -327,7 +327,7 @@ pub fn openxr_run(
|
||||
)?;
|
||||
|
||||
let ipd = helpers::ipd_from_views(&views);
|
||||
if (app.input_state.ipd - ipd).abs() > 0.01 {
|
||||
if (app.input_state.ipd - ipd).abs() > 0.05 {
|
||||
log::info!("IPD changed: {} -> {}", app.input_state.ipd, ipd);
|
||||
app.input_state.ipd = ipd;
|
||||
Toast::new(ToastTopic::IpdChange, "IPD".into(), format!("{ipd:.1} mm"))
|
||||
|
||||
@@ -329,10 +329,10 @@ pub trait OverlayBackend {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub enum Positioning {
|
||||
/// Stays in place unless recentered, recenters relative to HMD
|
||||
/// Stays in place, recenters relative to HMD
|
||||
#[default]
|
||||
Floating,
|
||||
/// Stays in place unless recentered, recenters relative to anchor
|
||||
/// Stays in place, recenters relative to anchor
|
||||
Anchored,
|
||||
/// Following HMD
|
||||
FollowHead { lerp: f32 },
|
||||
|
||||
9
wlx-overlay-s/src/backend/set.rs
Normal file
9
wlx-overlay-s/src/backend/set.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use glam::Affine3A;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct OverlaySetItem {
|
||||
name: Arc<str>,
|
||||
transform: Affine3A,
|
||||
}
|
||||
|
||||
pub struct OverlaySet {}
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use glam::{Vec2, vec2};
|
||||
use glam::{vec2, Vec2};
|
||||
use vulkano::{
|
||||
buffer::{BufferCreateInfo, BufferUsage},
|
||||
command_buffer::{CommandBufferUsage, PrimaryAutoCommandBuffer, PrimaryCommandBufferAbstract},
|
||||
@@ -26,12 +26,12 @@ use crate::shaders::{frag_color, frag_grid, frag_screen, frag_srgb, vert_quad};
|
||||
use {ash::vk, std::os::raw::c_void};
|
||||
|
||||
use vulkano::{
|
||||
self, VulkanObject,
|
||||
self,
|
||||
buffer::{Buffer, BufferContents, IndexBuffer, Subbuffer},
|
||||
device::{
|
||||
physical::{PhysicalDevice, PhysicalDeviceType},
|
||||
Device, DeviceCreateInfo, DeviceExtensions, DeviceFeatures, Queue, QueueCreateInfo,
|
||||
QueueFlags,
|
||||
physical::{PhysicalDevice, PhysicalDeviceType},
|
||||
},
|
||||
format::Format,
|
||||
instance::{Instance, InstanceCreateInfo, InstanceExtensions},
|
||||
@@ -40,6 +40,7 @@ use vulkano::{
|
||||
vertex_input::Vertex,
|
||||
},
|
||||
shader::ShaderModule,
|
||||
VulkanObject,
|
||||
};
|
||||
|
||||
use dmabuf::get_drm_formats;
|
||||
@@ -672,3 +673,21 @@ impl ExtentExt for Arc<ImageView> {
|
||||
[w, h]
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtentExt for [u32; 3] {
|
||||
fn extent_f32(&self) -> [f32; 2] {
|
||||
let [w, h, _] = *self;
|
||||
[w as _, h as _]
|
||||
}
|
||||
fn extent_vec2(&self) -> Vec2 {
|
||||
let [w, h, _] = *self;
|
||||
Vec2 {
|
||||
x: w as _,
|
||||
y: h as _,
|
||||
}
|
||||
}
|
||||
fn extent_u32arr(&self) -> [u32; 2] {
|
||||
let [w, h, _] = *self;
|
||||
[w, h]
|
||||
}
|
||||
}
|
||||
|
||||
110
wlx-overlay-s/src/gui/README.md
Normal file
110
wlx-overlay-s/src/gui/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# WayVR GUI Customization
|
||||
|
||||
Place custom XML files under ~/.config/wayvr/gui
|
||||
|
||||
## Custom timezones, 12 vs 24-hour clock
|
||||
|
||||
These are not done via the GUI system, but via the regular config.
|
||||
|
||||
Create `~/.config/wayvr/conf.d/clock.yaml` as such:
|
||||
|
||||
```yaml
|
||||
timezones:
|
||||
- "Europe/Oslo"
|
||||
- "America/New_York"
|
||||
|
||||
clock_12h: false
|
||||
```
|
||||
|
||||
Once this file is created, the various settings in custom UI that accept the `_timezone` property will use these custom alternate timezones (instead of the default set, which are selected as major ones on different continents from your current actual timezone).
|
||||
|
||||
The first timezone is selected with `_timezone="0"`, the second with `_timezone="1"`, and so on.
|
||||
|
||||
There is usually no need to specify your own local timezone in here; omitting `_timezone` from a `_source="clock"` Label will display local time.
|
||||
|
||||
## Custom UI Elements
|
||||
|
||||
### Labels
|
||||
|
||||
#### Clock label
|
||||
|
||||
Clock labels are driven by the current time. Available display values are: `name` (timezone name), `time`, `date`, `dow`
|
||||
|
||||
See the Custom Timezones section for more info on timezones. Skip `_timezone` to use local time.
|
||||
|
||||
```xml
|
||||
<label _source="clock" _display="time" _timezone="0" [...] />
|
||||
```
|
||||
|
||||
#### Fifo label
|
||||
|
||||
Fifo label creates a fifo on your system that other programs can pipe output into.
|
||||
|
||||
- The label will look for the last line that has a trailing `\n` and display it as its text.
|
||||
- The pipe is only actively read while the HMD is active.
|
||||
- If the producer fills up the pipe buffer before the headset is activated, a SIGPIPE will be sent to the producer, which shall be handled gracefully.
|
||||
- If the pipe breaks for any reason, re-creation is attempted after 15 seconds.
|
||||
|
||||
```xml
|
||||
<label _source="fifo" _path="$XDG_RUNTIME_DIR/my-test-label" [...] />
|
||||
```
|
||||
|
||||
Example script to test with:
|
||||
```bash
|
||||
for i in {0..99}; do echo "i is $i" > $XDG_RUNTIME_DIR/my-test-label; sleep 1; done
|
||||
```
|
||||
|
||||
#### Shell Exec label
|
||||
|
||||
This label executes a shell script using the `sh` shell.
|
||||
|
||||
- Write lines to the script's stdout to update the label text.
|
||||
- The label will look for the last line that has a trailing `\n` and display it as its text.
|
||||
- Long-running scripts are allowed, but the stdout buffer is only read from while the headset is active.
|
||||
- As a consequence, the buffer may fill up during very long periods of inactivity, hanging the script due to IO wait until the headset is activated.
|
||||
- If the script exits successfully (code 0), it will be re-ran on the next frame.
|
||||
- Control the pacing from inside the script itself. For example, adding a sleep 5 will make the script execute at most once per 5 seconds.
|
||||
|
||||
```xml
|
||||
<label _source="shell" _exec="$HOME/.local/bin/my-test-script.sh" [...] />
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/usr/bin/bash
|
||||
echo "This is my script's output!"
|
||||
```
|
||||
|
||||
#### Battery label
|
||||
|
||||
This is a label type that's used internally to display battery states.
|
||||
|
||||
```xml
|
||||
<label _source="battery" _device="0" [...] />
|
||||
```
|
||||
|
||||
#### IPD
|
||||
|
||||
Displays IPD value in millimeters. Not parametrizable.
|
||||
|
||||
Format: `ipd`
|
||||
|
||||
```xml
|
||||
<label _source="ipd" [...] />
|
||||
```
|
||||
|
||||
### Buttons
|
||||
|
||||
Buttons consist of a label component and and one or more actions to handle press or release events.
|
||||
|
||||
If a shell-type press/release event's script writes to stdout, the last line of stdout will be set as the label text.
|
||||
|
||||
Long-running processes are allowed, but a new execution will not be triggered until the previous process has exited.
|
||||
|
||||
Note: As of WlxOverlay 25.10, we no longer support events based on laser color, as this was bad practice accessibility-wise.
|
||||
|
||||
Supported events:
|
||||
|
||||
```xml
|
||||
<button _press="" _release="" />
|
||||
```
|
||||
|
||||
160
wlx-overlay-s/src/gui/panel/button.rs
Normal file
160
wlx-overlay-s/src/gui/panel/button.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
io::BufReader,
|
||||
process::{Child, ChildStdout},
|
||||
};
|
||||
|
||||
use wgui::{
|
||||
event::{self, EventCallback, EventListenerCollection, EventListenerKind, ListenerHandleVec},
|
||||
parser::CustomAttribsInfoOwned,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{common::OverlaySelector, overlay::OverlayID, task::TaskType, wayvr::WayVRAction},
|
||||
config::{save_layout, AStrSetExt},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::helper::read_label_from_pipe;
|
||||
|
||||
pub(super) fn setup_custom_button<S>(
|
||||
attribs: &CustomAttribsInfoOwned,
|
||||
listeners: &mut EventListenerCollection<AppState, S>,
|
||||
listener_handles: &mut ListenerHandleVec,
|
||||
app: &AppState,
|
||||
) {
|
||||
const EVENTS: [(&str, EventListenerKind); 2] = [
|
||||
("press", EventListenerKind::MousePress),
|
||||
("release", EventListenerKind::MouseRelease),
|
||||
];
|
||||
|
||||
for (name, kind) in EVENTS.iter() {
|
||||
let Some(action) = attribs.get_value(name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut args = action.split_whitespace();
|
||||
|
||||
let Some(command) = args.next() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let callback: EventCallback<AppState, S> = match command {
|
||||
"::DashToggle" => Box::new(move |_common, _data, app, _| {
|
||||
app.tasks
|
||||
.enqueue(TaskType::WayVR(WayVRAction::ToggleDashboard));
|
||||
Ok(())
|
||||
}),
|
||||
"::OverlayToggle" => {
|
||||
let Some(selector) = args.next() else {
|
||||
log::warn!("Missing argument for {}", command);
|
||||
continue;
|
||||
};
|
||||
|
||||
let selector = selector
|
||||
.parse::<usize>()
|
||||
.map(|id| OverlaySelector::Id(OverlayID { 0: id }))
|
||||
.unwrap_or_else(|_| OverlaySelector::Name(selector.into()));
|
||||
|
||||
Box::new(move |_common, _data, app, _| {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
selector.clone(),
|
||||
Box::new(|app, o| {
|
||||
o.want_visible = !o.want_visible;
|
||||
if o.recenter {
|
||||
o.show_hide = o.want_visible;
|
||||
o.reset(app, false);
|
||||
}
|
||||
|
||||
let mut state_dirty = false;
|
||||
if !o.want_visible {
|
||||
state_dirty |=
|
||||
app.session.config.show_screens.arc_rm(o.name.as_ref());
|
||||
} else if o.want_visible {
|
||||
state_dirty |=
|
||||
app.session.config.show_screens.arc_set(o.name.clone());
|
||||
}
|
||||
|
||||
if state_dirty {
|
||||
match save_layout(&app.session.config) {
|
||||
Ok(()) => log::debug!("Saved state"),
|
||||
Err(e) => {
|
||||
log::error!("Failed to save state: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"::WatchHide" => todo!(),
|
||||
"::WatchSwapHand" => todo!(),
|
||||
"::EditToggle" => return,
|
||||
"::OscSend" => return,
|
||||
// shell
|
||||
_ => todo!(),
|
||||
};
|
||||
|
||||
listeners.register(listener_handles, attribs.widget_id, *kind, callback);
|
||||
}
|
||||
}
|
||||
struct ShellButtonMutableState {
|
||||
child: Option<Child>,
|
||||
reader: Option<BufReader<ChildStdout>>,
|
||||
}
|
||||
|
||||
struct ShellButtonState {
|
||||
exec: String,
|
||||
mut_state: RefCell<ShellButtonMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
fn shell_on_action(
|
||||
state: &ShellButtonState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
}
|
||||
|
||||
fn shell_on_tick(
|
||||
state: &ShellButtonState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
if let Some(mut child) = mut_state.child.take() {
|
||||
match child.try_wait() {
|
||||
// not exited yet
|
||||
Ok(None) => {
|
||||
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
|
||||
read_label_from_pipe("child process", r, &mut *state.carry_over.borrow_mut())
|
||||
}) {
|
||||
//TODO update label
|
||||
}
|
||||
mut_state.child = Some(child);
|
||||
}
|
||||
// exited successfully
|
||||
Ok(Some(code)) if code.success() => {
|
||||
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
|
||||
read_label_from_pipe("child process", r, &mut *state.carry_over.borrow_mut())
|
||||
}) {
|
||||
//TODO update label
|
||||
}
|
||||
mut_state.child = None;
|
||||
}
|
||||
// exited with failure
|
||||
Ok(Some(code)) => {
|
||||
mut_state.child = None;
|
||||
log::warn!("Label process exited with code {}", code);
|
||||
}
|
||||
// lost
|
||||
Err(_) => {
|
||||
mut_state.child = None;
|
||||
log::warn!("Label child process lost.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
wlx-overlay-s/src/gui/panel/helper.rs
Normal file
53
wlx-overlay-s/src/gui/panel/helper.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
static ENV_VAR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)}|\$([A-Z_][A-Z0-9_]*)").unwrap() // want panic
|
||||
});
|
||||
|
||||
pub(super) fn expand_env_vars(template: &str) -> String {
|
||||
ENV_VAR_REGEX
|
||||
.replace_all(template, |caps: ®ex::Captures| {
|
||||
let var_name = caps.get(1).or(caps.get(2)).unwrap().as_str();
|
||||
std::env::var(var_name)
|
||||
.inspect_err(|e| log::warn!("Unable to substitute env var {var_name}: {e:?}"))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
pub(super) fn read_label_from_pipe<R>(
|
||||
path: &str,
|
||||
reader: &mut BufReader<R>,
|
||||
carry_over: &mut Option<String>,
|
||||
) -> Option<String>
|
||||
where
|
||||
R: Read + Sized,
|
||||
{
|
||||
let mut prev = String::new();
|
||||
let mut cur = String::new();
|
||||
|
||||
for r in reader.lines() {
|
||||
match r {
|
||||
Ok(line) => {
|
||||
prev = cur;
|
||||
cur = carry_over.take().map(|s| s + &line).unwrap_or(line);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("pipe read error on {path}: {e:?}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
carry_over.replace(cur);
|
||||
|
||||
if prev.len() > 0 {
|
||||
Some(prev)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
434
wlx-overlay-s/src/gui/panel/label.rs
Normal file
434
wlx-overlay-s/src/gui/panel/label.rs
Normal file
@@ -0,0 +1,434 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs, io,
|
||||
os::unix::fs::FileTypeExt,
|
||||
process::{Child, ChildStdout, Command, Stdio},
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use interprocess::os::unix::fifo_file::create_fifo;
|
||||
use wgui::{
|
||||
drawing,
|
||||
event::{self, EventCallback, EventListenerCollection, ListenerHandleVec},
|
||||
i18n::Translation,
|
||||
layout::Layout,
|
||||
parser::{parse_color_hex, CustomAttribsInfoOwned},
|
||||
widget::label::WidgetLabel,
|
||||
};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::helper::{expand_env_vars, read_label_from_pipe};
|
||||
|
||||
pub(super) fn setup_custom_label<S>(
|
||||
layout: &mut Layout,
|
||||
attribs: &CustomAttribsInfoOwned,
|
||||
listeners: &mut EventListenerCollection<AppState, S>,
|
||||
listener_handles: &mut ListenerHandleVec,
|
||||
app: &AppState,
|
||||
) {
|
||||
let Some(source) = attribs.get_value("source") else {
|
||||
log::warn!("custom label with no source!");
|
||||
return;
|
||||
};
|
||||
|
||||
let callback: EventCallback<AppState, S> = match source {
|
||||
"shell" => {
|
||||
let Some(exec) = attribs.get_value("exec") else {
|
||||
log::warn!("label with shell source but no exec attribute!");
|
||||
return;
|
||||
};
|
||||
let state = ShellLabelState {
|
||||
exec: exec.to_string(),
|
||||
mut_state: RefCell::new(ShellLabelMutableState {
|
||||
child: None,
|
||||
reader: None,
|
||||
next_try: Instant::now(),
|
||||
}),
|
||||
carry_over: RefCell::new(None),
|
||||
};
|
||||
Box::new(move |common, data, _app, _| {
|
||||
shell_on_tick(&state, common, data);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"fifo" => {
|
||||
let Some(path) = attribs.get_value("path") else {
|
||||
log::warn!("label with fifo source but no path attribute!");
|
||||
return;
|
||||
};
|
||||
let state = FifoLabelState {
|
||||
path: expand_env_vars(path),
|
||||
carry_over: RefCell::new(None),
|
||||
mut_state: RefCell::new(FifoLabelMutableState {
|
||||
reader: None,
|
||||
next_try: Instant::now(),
|
||||
}),
|
||||
};
|
||||
Box::new(move |common, data, _app, _| {
|
||||
pipe_on_tick(&state, common, data);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"battery" => {
|
||||
let Some(device) = attribs
|
||||
.get_value("device")
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
else {
|
||||
log::warn!("label with battery source but no device attribute!");
|
||||
return;
|
||||
};
|
||||
|
||||
let state = BatteryLabelState {
|
||||
low_color: attribs
|
||||
.get_value("low_color")
|
||||
.and_then(|s| parse_color_hex(s))
|
||||
.unwrap_or(BAT_LOW),
|
||||
normal_color: attribs
|
||||
.get_value("normal_color")
|
||||
.and_then(|s| parse_color_hex(s))
|
||||
.unwrap_or(BAT_NORMAL),
|
||||
charging_color: attribs
|
||||
.get_value("charging_color")
|
||||
.and_then(|s| parse_color_hex(s))
|
||||
.unwrap_or(BAT_CHARGING),
|
||||
low_threshold: attribs
|
||||
.get_value("low_threshold")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(BAT_LOW_THRESHOLD),
|
||||
device,
|
||||
};
|
||||
Box::new(move |common, data, app, _| {
|
||||
battery_on_tick(&state, common, data, app);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"clock" => {
|
||||
let Some(display) = attribs.get_value("display") else {
|
||||
log::warn!("label with clock source but no display attribute!");
|
||||
return;
|
||||
};
|
||||
|
||||
let format = match display {
|
||||
"name" => {
|
||||
let maybe_pretty_tz = attribs
|
||||
.get_value("timezone")
|
||||
.and_then(|tz| tz.parse::<usize>().ok())
|
||||
.and_then(|tz_idx| app.session.config.timezones.get(tz_idx))
|
||||
.and_then(|tz_name| {
|
||||
tz_name.split('/').next_back().map(|x| x.replace('_', " "))
|
||||
});
|
||||
|
||||
let pretty_tz = match maybe_pretty_tz.as_ref() {
|
||||
Some(x) => x.as_str(),
|
||||
None => "Local",
|
||||
};
|
||||
|
||||
let mut i18n = layout.state.globals.i18n();
|
||||
layout
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<WidgetLabel>(attribs.widget_id)
|
||||
.unwrap()
|
||||
.set_text_simple(&mut *i18n, Translation::from_raw_text(&pretty_tz));
|
||||
|
||||
// does not need to be dynamic
|
||||
return;
|
||||
}
|
||||
"date" => "%x",
|
||||
"dow" => "%A",
|
||||
"time" => {
|
||||
if app.session.config.clock_12h {
|
||||
"%I:%M %p"
|
||||
} else {
|
||||
"%H:%M"
|
||||
}
|
||||
}
|
||||
unk => {
|
||||
log::warn!("Unknown display value for clock label source: {unk}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let tz_str = attribs
|
||||
.get_value("timezone")
|
||||
.and_then(|tz| tz.parse::<usize>().ok())
|
||||
.and_then(|tz_idx| app.session.config.timezones.get(tz_idx));
|
||||
|
||||
let state = ClockLabelState {
|
||||
timezone: tz_str.and_then(|tz| {
|
||||
tz.parse()
|
||||
.inspect_err(|e| log::warn!("Invalid timezone: {e:?}"))
|
||||
.ok()
|
||||
}),
|
||||
format: format.into(),
|
||||
};
|
||||
|
||||
Box::new(move |common, data, _app, _| {
|
||||
clock_on_tick(&state, common, data);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"ipd" => Box::new(|common, data, app, _| {
|
||||
ipd_on_tick(common, data, app);
|
||||
Ok(())
|
||||
}),
|
||||
unk => {
|
||||
log::warn!("Unknown source value for label: {unk}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
listeners.register(
|
||||
listener_handles,
|
||||
attribs.widget_id,
|
||||
wgui::event::EventListenerKind::InternalStateChange,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
struct ShellLabelMutableState {
|
||||
child: Option<Child>,
|
||||
reader: Option<io::BufReader<ChildStdout>>,
|
||||
next_try: Instant,
|
||||
}
|
||||
|
||||
struct ShellLabelState {
|
||||
exec: String,
|
||||
mut_state: RefCell<ShellLabelMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
fn shell_on_tick(
|
||||
state: &ShellLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
if let Some(mut child) = mut_state.child.take() {
|
||||
match child.try_wait() {
|
||||
// not exited yet
|
||||
Ok(None) => {
|
||||
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
|
||||
read_label_from_pipe("child process", r, &mut *state.carry_over.borrow_mut())
|
||||
}) {
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
mut_state.child = Some(child);
|
||||
return;
|
||||
}
|
||||
// exited successfully
|
||||
Ok(Some(code)) if code.success() => {
|
||||
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
|
||||
read_label_from_pipe("child process", r, &mut *state.carry_over.borrow_mut())
|
||||
}) {
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
mut_state.child = None;
|
||||
return;
|
||||
}
|
||||
// exited with failure
|
||||
Ok(Some(code)) => {
|
||||
mut_state.child = None;
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
log::warn!("Label process exited with code {}", code);
|
||||
return;
|
||||
}
|
||||
// lost
|
||||
Err(_) => {
|
||||
mut_state.child = None;
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
log::warn!("Label child process lost.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if mut_state.next_try > Instant::now() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&state.exec)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(mut child) => {
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
mut_state.child = Some(child);
|
||||
mut_state.reader = Some(io::BufReader::new(stdout));
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to run shell script '{}': {e:?}", &state.exec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FifoLabelMutableState {
|
||||
reader: Option<io::BufReader<fs::File>>,
|
||||
next_try: Instant,
|
||||
}
|
||||
|
||||
struct FifoLabelState {
|
||||
path: String,
|
||||
mut_state: RefCell<FifoLabelMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl FifoLabelState {
|
||||
fn try_remove_fifo(&self) -> anyhow::Result<()> {
|
||||
let meta = match fs::metadata(&self.path) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
if fs::exists(&self.path).unwrap_or(true) {
|
||||
anyhow::bail!("Could not stat existing file at {}: {e:?}", &self.path);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if !meta.file_type().is_fifo() {
|
||||
anyhow::bail!("Existing file at {} is not a FIFO", &self.path);
|
||||
}
|
||||
|
||||
if let Err(e) = fs::remove_file(&self.path) {
|
||||
anyhow::bail!("Unable to remove existing FIFO at {}: {e:?}", &self.path);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FifoLabelState {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = self.try_remove_fifo() {
|
||||
log::debug!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pipe_on_tick(
|
||||
state: &FifoLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
let reader = match mut_state.reader.as_mut() {
|
||||
Some(f) => f,
|
||||
None => {
|
||||
if mut_state.next_try > Instant::now() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = state.try_remove_fifo() {
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
log::warn!("Requested FIFO path is taken: {e:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = create_fifo(&state.path, 0o777) {
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
log::warn!("Failed to create FIFO: {e:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
mut_state.reader = fs::File::open(&state.path)
|
||||
.inspect_err(|e| {
|
||||
log::warn!("Failed to open FIFO: {e:?}");
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
})
|
||||
.map(|f| io::BufReader::new(f))
|
||||
.ok();
|
||||
|
||||
mut_state.reader.as_mut().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(text) =
|
||||
read_label_from_pipe(&state.path, reader, &mut *state.carry_over.borrow_mut())
|
||||
{
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
}
|
||||
|
||||
const BAT_LOW: drawing::Color = drawing::Color::new(0.69, 0.38, 0.38, 1.);
|
||||
const BAT_NORMAL: drawing::Color = drawing::Color::new(0.55, 0.84, 0.79, 1.);
|
||||
const BAT_CHARGING: drawing::Color = drawing::Color::new(0.38, 0.50, 0.62, 1.);
|
||||
const BAT_LOW_THRESHOLD: u32 = 30;
|
||||
|
||||
struct BatteryLabelState {
|
||||
device: usize,
|
||||
low_color: drawing::Color,
|
||||
normal_color: drawing::Color,
|
||||
charging_color: drawing::Color,
|
||||
low_threshold: u32,
|
||||
}
|
||||
|
||||
fn battery_on_tick(
|
||||
state: &BatteryLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
app: &AppState,
|
||||
) {
|
||||
let device = app.input_state.devices.get(state.device);
|
||||
|
||||
let tags = ["", "H", "L", "R", "T"];
|
||||
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
|
||||
if let Some(device) = device {
|
||||
if let Some(soc) = device.soc {
|
||||
let soc = (soc * 100.).min(99.) as u32;
|
||||
let text = format!("{}{}", tags[device.role as usize], soc);
|
||||
let color = if device.charging {
|
||||
state.charging_color
|
||||
} else if soc < state.low_threshold {
|
||||
state.low_color
|
||||
} else {
|
||||
state.normal_color
|
||||
};
|
||||
label.set_color(common, color, false);
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
return;
|
||||
}
|
||||
}
|
||||
label.set_text(common, Translation::default());
|
||||
}
|
||||
|
||||
struct ClockLabelState {
|
||||
timezone: Option<Tz>,
|
||||
format: Rc<str>,
|
||||
}
|
||||
|
||||
fn clock_on_tick(
|
||||
state: &ClockLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let date_time = state.timezone.as_ref().map_or_else(
|
||||
|| format!("{}", Local::now().format(&state.format)),
|
||||
|tz| format!("{}", Local::now().with_timezone(tz).format(&state.format)),
|
||||
);
|
||||
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&date_time));
|
||||
}
|
||||
|
||||
fn ipd_on_tick(
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
app: &AppState,
|
||||
) {
|
||||
let text = app.input_state.ipd.to_string();
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
@@ -1,22 +1,26 @@
|
||||
use std::sync::Arc;
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
|
||||
use glam::{Affine2, Vec2, vec2};
|
||||
use button::setup_custom_button;
|
||||
use glam::{vec2, Affine2, Vec2};
|
||||
use label::setup_custom_label;
|
||||
use vulkano::{command_buffer::CommandBufferUsage, image::view::ImageView};
|
||||
use wgui::{
|
||||
drawing,
|
||||
event::{
|
||||
Event as WguiEvent, EventListenerCollection, InternalStateChangeEvent, ListenerHandleVec,
|
||||
MouseButtonIndex, MouseDownEvent, MouseLeaveEvent, MouseMotionEvent, MouseUpEvent,
|
||||
MouseWheelEvent,
|
||||
},
|
||||
layout::{Layout, LayoutParams},
|
||||
layout::{Layout, LayoutParams, WidgetID},
|
||||
parser::ParserState,
|
||||
renderer_vk::context::Context as WguiContext,
|
||||
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
input::{Haptics, PointerHit, PointerMode},
|
||||
overlay::{FrameMeta, OverlayBackend, ShouldRender, ui_transform},
|
||||
overlay::{ui_transform, FrameMeta, OverlayBackend, ShouldRender},
|
||||
},
|
||||
graphics::{CommandBuffers, ExtentExt},
|
||||
state::AppState,
|
||||
@@ -24,9 +28,15 @@ use crate::{
|
||||
|
||||
use super::{timer::GuiTimer, timestep::Timestep};
|
||||
|
||||
mod button;
|
||||
mod helper;
|
||||
mod label;
|
||||
|
||||
const MAX_SIZE: u32 = 2048;
|
||||
const MAX_SIZE_VEC2: Vec2 = vec2(MAX_SIZE as _, MAX_SIZE as _);
|
||||
|
||||
const COLOR_ERR: drawing::Color = drawing::Color::new(1., 0., 1., 1.);
|
||||
|
||||
pub struct GuiPanel<S> {
|
||||
pub layout: Layout,
|
||||
pub state: S,
|
||||
@@ -39,20 +49,88 @@ pub struct GuiPanel<S> {
|
||||
timestep: Timestep,
|
||||
}
|
||||
|
||||
impl<S> GuiPanel<S> {
|
||||
pub fn new_from_template(app: &mut AppState, path: &str, state: S) -> anyhow::Result<Self> {
|
||||
let mut listeners = EventListenerCollection::<AppState, S>::default();
|
||||
pub type OnCustomIdFunc<S> = Box<
|
||||
dyn Fn(
|
||||
Rc<str>,
|
||||
WidgetID,
|
||||
&wgui::parser::ParseDocumentParams,
|
||||
&mut Layout,
|
||||
&mut ParserState,
|
||||
&mut EventListenerCollection<AppState, S>,
|
||||
) -> anyhow::Result<()>,
|
||||
>;
|
||||
|
||||
let (layout, parser_state) = wgui::parser::new_layout_from_assets(
|
||||
&mut listeners,
|
||||
&wgui::parser::ParseDocumentParams {
|
||||
globals: app.wgui_globals.clone(),
|
||||
path,
|
||||
extra: Default::default(),
|
||||
impl<S> GuiPanel<S> {
|
||||
pub fn new_from_template(
|
||||
app: &mut AppState,
|
||||
path: &str,
|
||||
state: S,
|
||||
on_custom_id: Option<OnCustomIdFunc<S>>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut listeners = EventListenerCollection::<AppState, S>::default();
|
||||
let mut listener_handles = ListenerHandleVec::default();
|
||||
|
||||
let custom_elems = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let doc_params = wgui::parser::ParseDocumentParams {
|
||||
globals: app.wgui_globals.clone(),
|
||||
path,
|
||||
extra: wgui::parser::ParseDocumentExtra {
|
||||
on_custom_attribs: Some(Box::new({
|
||||
let custom_elems = custom_elems.clone();
|
||||
move |attribs| {
|
||||
custom_elems.borrow_mut().push(attribs.to_owned());
|
||||
}
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
let (mut layout, mut parser_state) = wgui::parser::new_layout_from_assets(
|
||||
&mut listeners,
|
||||
&doc_params,
|
||||
&LayoutParams::default(),
|
||||
)?;
|
||||
|
||||
if let Some(on_element_id) = on_custom_id {
|
||||
let ids = parser_state.ids.clone();
|
||||
|
||||
for (id, widget) in ids {
|
||||
on_element_id(
|
||||
id.clone(),
|
||||
widget,
|
||||
&doc_params,
|
||||
&mut layout,
|
||||
&mut parser_state,
|
||||
&mut listeners,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for elem in custom_elems.borrow().iter() {
|
||||
if layout
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<WidgetLabel>(elem.widget_id)
|
||||
.is_some()
|
||||
{
|
||||
setup_custom_label(
|
||||
&mut layout,
|
||||
elem,
|
||||
&mut listeners,
|
||||
&mut listener_handles,
|
||||
app,
|
||||
);
|
||||
} else if layout
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<WidgetRectangle>(elem.widget_id)
|
||||
.is_some()
|
||||
{
|
||||
setup_custom_button(elem, &mut listeners, &mut listener_handles, app);
|
||||
}
|
||||
}
|
||||
|
||||
let context = WguiContext::new(&mut app.wgui_shared, 1.0)?;
|
||||
let mut timestep = Timestep::new();
|
||||
timestep.set_tps(60.0);
|
||||
@@ -62,7 +140,7 @@ impl<S> GuiPanel<S> {
|
||||
context,
|
||||
timestep,
|
||||
state,
|
||||
listener_handles: ListenerHandleVec::default(),
|
||||
listener_handles,
|
||||
parser_state,
|
||||
timers: vec![],
|
||||
listeners,
|
||||
@@ -105,7 +183,7 @@ impl<S> GuiPanel<S> {
|
||||
|
||||
impl<S> OverlayBackend for GuiPanel<S> {
|
||||
fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
if self.layout.content_size.x * self.layout.content_size.y == 0.0 {
|
||||
if self.layout.content_size.x * self.layout.content_size.y != 0.0 {
|
||||
self.update_layout()?;
|
||||
self.interaction_transform = Some(ui_transform([
|
||||
//TODO: dynamic
|
||||
@@ -11,7 +11,7 @@ pub fn create_anchor<O>(app: &mut AppState) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let panel = GuiPanel::new_from_template(app, "gui/anchor.xml", ())?;
|
||||
let panel = GuiPanel::new_from_template(app, "gui/anchor.xml", (), None)?;
|
||||
|
||||
Ok(OverlayData {
|
||||
state: OverlayState {
|
||||
|
||||
@@ -16,7 +16,7 @@ where
|
||||
O: Default,
|
||||
{
|
||||
let state = BarState {};
|
||||
let mut panel = GuiPanel::new_from_template(app, "gui/bar.xml", state)?;
|
||||
let mut panel = GuiPanel::new_from_template(app, "gui/bar.xml", state, None)?;
|
||||
|
||||
for (id, _widget_id) in &panel.parser_state.ids {
|
||||
match id.as_ref() {
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
use std::{
|
||||
sync::{Arc, LazyLock, atomic::AtomicU64},
|
||||
sync::{atomic::AtomicU64, Arc, LazyLock},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use glam::{Affine2, Vec2, vec2};
|
||||
use glam::{vec2, Affine2, Vec2};
|
||||
use vulkano::image::view::ImageView;
|
||||
use wlx_capture::WlxCapture;
|
||||
use wlx_capture::{frame::Transform, WlxCapture};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
input::{Haptics, PointerHit, PointerMode},
|
||||
overlay::{FrameMeta, OverlayBackend, ShouldRender},
|
||||
},
|
||||
graphics::CommandBuffers,
|
||||
graphics::{CommandBuffers, ExtentExt},
|
||||
state::AppState,
|
||||
subsystem::hid::{MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT},
|
||||
};
|
||||
|
||||
use super::{
|
||||
Transform,
|
||||
capture::{ScreenPipeline, WlxCaptureIn, WlxCaptureOut, receive_callback},
|
||||
};
|
||||
use super::capture::{receive_callback, ScreenPipeline, WlxCaptureIn, WlxCaptureOut};
|
||||
|
||||
const CURSOR_SIZE: f32 = 16. / 1440.;
|
||||
|
||||
@@ -66,17 +63,17 @@ impl ScreenBackend {
|
||||
|
||||
pub(super) fn set_mouse_transform(&mut self, pos: Vec2, size: Vec2, transform: Transform) {
|
||||
self.mouse_transform = match transform {
|
||||
Transform::_90 | Transform::Flipped90 => Affine2::from_cols(
|
||||
Transform::Rotated90 | Transform::Flipped90 => Affine2::from_cols(
|
||||
vec2(0., size.y),
|
||||
vec2(-size.x, 0.),
|
||||
vec2(pos.x + size.x, pos.y),
|
||||
),
|
||||
Transform::_180 | Transform::Flipped180 => Affine2::from_cols(
|
||||
Transform::Rotated180 | Transform::Flipped180 => Affine2::from_cols(
|
||||
vec2(-size.x, 0.),
|
||||
vec2(0., -size.y),
|
||||
vec2(pos.x + size.x, pos.y + size.y),
|
||||
),
|
||||
Transform::_270 | Transform::Flipped270 => Affine2::from_cols(
|
||||
Transform::Rotated270 | Transform::Flipped270 => Affine2::from_cols(
|
||||
vec2(0., -size.y),
|
||||
vec2(size.x, 0.),
|
||||
vec2(pos.x, pos.y + size.y),
|
||||
@@ -85,16 +82,16 @@ impl ScreenBackend {
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) fn get_interaction_transform(&mut self, res: Vec2, transform: Transform) {
|
||||
pub(super) fn set_interaction_transform(&mut self, res: Vec2, transform: Transform) {
|
||||
let center = Vec2 { x: 0.5, y: 0.5 };
|
||||
self.interaction_transform = Some(match transform {
|
||||
Transform::_90 | Transform::Flipped90 => {
|
||||
Transform::Rotated90 | Transform::Flipped90 => {
|
||||
Affine2::from_cols(Vec2::NEG_Y * (res.x / res.y), Vec2::NEG_X, center)
|
||||
}
|
||||
Transform::_180 | Transform::Flipped180 => {
|
||||
Transform::Rotated180 | Transform::Flipped180 => {
|
||||
Affine2::from_cols(Vec2::NEG_X, Vec2::NEG_Y * (-res.x / res.y), center)
|
||||
}
|
||||
Transform::_270 | Transform::Flipped270 => {
|
||||
Transform::Rotated270 | Transform::Flipped270 => {
|
||||
Affine2::from_cols(Vec2::Y * (res.x / res.y), Vec2::X, center)
|
||||
}
|
||||
_ if res.y > res.x => {
|
||||
@@ -164,9 +161,14 @@ impl OverlayBackend for ScreenBackend {
|
||||
if let Some(pipeline) = self.pipeline.as_mut() {
|
||||
if self.meta.is_some_and(|old| old.extent != meta.extent) {
|
||||
pipeline.set_extent(app, [meta.extent[0] as _, meta.extent[1] as _])?;
|
||||
self.set_interaction_transform(
|
||||
meta.extent.extent_vec2(),
|
||||
frame.get_transform(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.pipeline = Some(ScreenPipeline::new(&meta, app)?);
|
||||
self.set_interaction_transform(meta.extent.extent_vec2(), frame.get_transform());
|
||||
}
|
||||
|
||||
self.meta = Some(meta);
|
||||
|
||||
@@ -6,22 +6,21 @@ use vulkano::{
|
||||
command_buffer::CommandBufferUsage,
|
||||
device::Queue,
|
||||
format::Format,
|
||||
image::{Image, sampler::Filter, view::ImageView},
|
||||
image::{sampler::Filter, view::ImageView, Image},
|
||||
pipeline::graphics::{color_blend::AttachmentBlend, input_assembly::PrimitiveTopology},
|
||||
};
|
||||
use wgui::gfx::{WGfx, pass::WGfxPass, pipeline::WGfxPipeline};
|
||||
use wgui::gfx::{pass::WGfxPass, pipeline::WGfxPipeline, WGfx};
|
||||
use wlx_capture::{
|
||||
frame::{self as wlx_frame, DrmFormat, FrameFormat, MouseMeta, Transform, WlxFrame},
|
||||
WlxCapture,
|
||||
frame::{self as wlx_frame, DrmFormat, FrameFormat, MouseMeta, WlxFrame},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::overlay::FrameMeta,
|
||||
config::GeneralConfig,
|
||||
graphics::{
|
||||
CommandBuffers, Vert2Uv,
|
||||
dmabuf::{WGfxDmabuf, fourcc_to_vk},
|
||||
upload_quad_vertices,
|
||||
dmabuf::{fourcc_to_vk, WGfxDmabuf},
|
||||
upload_quad_vertices, CommandBuffers, Vert2Uv,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -209,6 +208,10 @@ impl WlxCaptureOut {
|
||||
format: self.image.format(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const fn get_transform(&self) -> Transform {
|
||||
self.format.transform
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_image(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::{f32::consts::PI, sync::Arc};
|
||||
|
||||
use backend::ScreenBackend;
|
||||
use glam::{Quat, Vec3, vec3a};
|
||||
use wayland_client::protocol::wl_output;
|
||||
use glam::{vec3a, Quat, Vec3};
|
||||
use wl::create_screens_wayland;
|
||||
use x11::{create_screens_x11pw, create_screens_xshm};
|
||||
use wlx_capture::frame::Transform;
|
||||
|
||||
use crate::{
|
||||
backend::overlay::{OverlayState, Positioning},
|
||||
@@ -21,41 +20,12 @@ pub mod wl;
|
||||
#[cfg(feature = "x11")]
|
||||
pub mod x11;
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Transform {
|
||||
Normal,
|
||||
_90,
|
||||
_180,
|
||||
_270,
|
||||
Flipped,
|
||||
Flipped90,
|
||||
Flipped180,
|
||||
Flipped270,
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
impl From<wl_output::Transform> for Transform {
|
||||
fn from(t: wl_output::Transform) -> Self {
|
||||
match t {
|
||||
wl_output::Transform::_90 => Self::_90,
|
||||
wl_output::Transform::_180 => Self::_180,
|
||||
wl_output::Transform::_270 => Self::_270,
|
||||
wl_output::Transform::Flipped => Self::Flipped,
|
||||
wl_output::Transform::Flipped90 => Self::Flipped90,
|
||||
wl_output::Transform::Flipped180 => Self::Flipped180,
|
||||
wl_output::Transform::Flipped270 => Self::Flipped270,
|
||||
_ => Self::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_screen_state(name: Arc<str>, transform: Transform, session: &AppSession) -> OverlayState {
|
||||
let angle = if session.config.upright_screen_fix {
|
||||
match transform {
|
||||
Transform::_90 | Transform::Flipped90 => PI / 2.,
|
||||
Transform::_180 | Transform::Flipped180 => PI,
|
||||
Transform::_270 | Transform::Flipped270 => -PI / 2.,
|
||||
Transform::Rotated90 | Transform::Flipped90 => PI / 2.,
|
||||
Transform::Rotated180 | Transform::Flipped180 => PI,
|
||||
Transform::Rotated270 | Transform::Flipped270 => -PI / 2.,
|
||||
_ => 0.,
|
||||
}
|
||||
} else {
|
||||
@@ -103,12 +73,12 @@ pub fn create_screens(app: &mut AppState) -> anyhow::Result<(ScreenCreateData, O
|
||||
.ok();
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
match create_screens_x11pw(app) {
|
||||
match x11::create_screens_x11pw(app) {
|
||||
Ok(data) => return Ok((data, keymap)),
|
||||
Err(e) => log::info!("Will not use X11 PipeWire capture: {e:?}"),
|
||||
}
|
||||
|
||||
Ok((create_screens_xshm(app)?, keymap))
|
||||
Ok((x11::create_screens_xshm(app)?, keymap))
|
||||
}
|
||||
#[cfg(not(feature = "x11"))]
|
||||
anyhow::bail!("No backends left to try.")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use glam::vec2;
|
||||
use wlx_capture::{
|
||||
WlxCapture,
|
||||
wayland::{WlxClient, WlxOutput},
|
||||
wlr_dmabuf::WlrDmabufCapture,
|
||||
wlr_screencopy::WlrScreencopyCapture,
|
||||
WlxCapture,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -13,10 +13,10 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
ScreenCreateData,
|
||||
backend::ScreenBackend,
|
||||
capture::{MainThreadWlxCapture, new_wlx_capture},
|
||||
capture::{new_wlx_capture, MainThreadWlxCapture},
|
||||
pw::{load_pw_token_config, save_pw_token_config},
|
||||
ScreenCreateData,
|
||||
};
|
||||
|
||||
impl ScreenBackend {
|
||||
@@ -126,7 +126,7 @@ pub fn create_screens_wayland(wl: &mut WlxClient, app: &mut AppState) -> ScreenC
|
||||
) {
|
||||
let logical_pos = vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32);
|
||||
let logical_size = vec2(output.logical_size.0 as f32, output.logical_size.1 as f32);
|
||||
let transform = output.transform.into();
|
||||
let transform = output.transform;
|
||||
|
||||
backend.set_mouse_transform(logical_pos, logical_size, transform);
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ use std::sync::Arc;
|
||||
|
||||
use glam::vec2;
|
||||
use wlx_capture::{
|
||||
WlxCapture,
|
||||
frame::Transform,
|
||||
xshm::{XshmCapture, XshmScreen},
|
||||
WlxCapture,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -12,9 +13,9 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
ScreenCreateData, Transform,
|
||||
backend::ScreenBackend,
|
||||
capture::{MainThreadWlxCapture, new_wlx_capture},
|
||||
capture::{new_wlx_capture, MainThreadWlxCapture},
|
||||
ScreenCreateData,
|
||||
};
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
use glam::{Affine3A, Quat, Vec3A};
|
||||
use wgui::{
|
||||
i18n::Translation,
|
||||
parser::parse_color_hex,
|
||||
renderer_vk::text::TextStyle,
|
||||
taffy::{
|
||||
self,
|
||||
prelude::{auto, length, percent},
|
||||
},
|
||||
widget::{
|
||||
rectangle::{Rectangle, RectangleParams},
|
||||
text::{TextLabel, TextParams},
|
||||
util::WLength,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::overlay::{OverlayBackend, OverlayState, Z_ORDER_TOAST},
|
||||
gui::panel::GuiPanel,
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
const FONT_SIZE: isize = 16;
|
||||
const PADDING: (f32, f32) = (25., 7.);
|
||||
const PIXELS_TO_METERS: f32 = 1. / 2000.;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn new_tooltip(
|
||||
text: &str,
|
||||
transform: Affine3A,
|
||||
app: &mut AppState,
|
||||
) -> Option<(OverlayState, Box<dyn OverlayBackend>)> {
|
||||
let mut panel = GuiPanel::new_blank(app, ()).ok()?;
|
||||
|
||||
let globals = panel.layout.state.globals.clone();
|
||||
let mut i18n = globals.i18n();
|
||||
|
||||
let (rect, _) = panel
|
||||
.layout
|
||||
.add_child(
|
||||
panel.layout.root_widget,
|
||||
Rectangle::create(RectangleParams {
|
||||
color: parse_color_hex("#1e2030").unwrap(),
|
||||
border_color: parse_color_hex("#5e7090").unwrap(),
|
||||
border: 1.0,
|
||||
round: WLength::Units(4.0),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap(),
|
||||
taffy::Style {
|
||||
align_items: Some(taffy::AlignItems::Center),
|
||||
justify_content: Some(taffy::JustifyContent::Center),
|
||||
flex_direction: taffy::FlexDirection::Column,
|
||||
padding: length(4.0),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
let _ = panel.layout.add_child(
|
||||
rect,
|
||||
TextLabel::create(
|
||||
&mut i18n,
|
||||
TextParams {
|
||||
content: Translation::from_raw_text(text),
|
||||
style: TextStyle {
|
||||
color: parse_color_hex("#ffffff"),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
taffy::Style {
|
||||
size: taffy::Size {
|
||||
width: percent(1.0),
|
||||
height: auto(),
|
||||
},
|
||||
padding: length(8.0),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
panel.update_layout().ok()?;
|
||||
|
||||
let state = OverlayState {
|
||||
name: "tooltip".into(),
|
||||
want_visible: true,
|
||||
spawn_scale: panel.layout.content_size.x * PIXELS_TO_METERS,
|
||||
spawn_rotation: Quat::IDENTITY,
|
||||
spawn_point: Vec3A::ZERO,
|
||||
z_order: Z_ORDER_TOAST,
|
||||
positioning: crate::backend::overlay::Positioning::Static,
|
||||
..Default::default()
|
||||
};
|
||||
let backend = Box::new(panel);
|
||||
|
||||
Some((state, backend))
|
||||
}
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
use std::{rc::Rc, time::Duration};
|
||||
use std::{collections::HashMap, rc::Rc, time::Duration};
|
||||
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use glam::Vec3A;
|
||||
use regex::Regex;
|
||||
use wgui::{
|
||||
event::{self, EventListenerKind},
|
||||
i18n::Translation,
|
||||
widget::label::WidgetLabel,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
backend::overlay::{OverlayData, OverlayState, Positioning, Z_ORDER_WATCH},
|
||||
backend::overlay::{OverlayData, OverlayID, OverlayState, Positioning, Z_ORDER_WATCH},
|
||||
gui::{panel::GuiPanel, timer::GuiTimer},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -25,80 +18,39 @@ pub fn create_watch<O>(app: &mut AppState) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let screens = app
|
||||
.screens
|
||||
.iter()
|
||||
.map(|s| s.id)
|
||||
.collect::<SmallVec<[OverlayID; 8]>>();
|
||||
|
||||
let state = WatchState {};
|
||||
let mut panel = GuiPanel::new_from_template(app, "gui/watch.xml", state)?;
|
||||
let mut panel = GuiPanel::new_from_template(
|
||||
app,
|
||||
"gui/watch.xml",
|
||||
state,
|
||||
Some(Box::new(
|
||||
move |id, widget, doc_params, layout, parser_state, listeners| {
|
||||
if &*id != "sets" {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (idx, handle) in screens.iter().enumerate() {
|
||||
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
|
||||
params.insert("display".into(), (idx + 1).to_string().into());
|
||||
params.insert("handle".into(), handle.0.to_string().into());
|
||||
parser_state
|
||||
.process_template(doc_params, "Set", layout, listeners, widget, params)?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)),
|
||||
)?;
|
||||
|
||||
panel
|
||||
.timers
|
||||
.push(GuiTimer::new(Duration::from_millis(100), 0));
|
||||
|
||||
let clock_regex = Regex::new(r"^clock([0-9])_([a-z]+)$").unwrap();
|
||||
|
||||
for (id, widget_id) in &panel.parser_state.ids {
|
||||
if let Some(cap) = clock_regex.captures(id) {
|
||||
let tz_idx: usize = cap.get(1).unwrap().as_str().parse().unwrap(); // safe due to regex
|
||||
let tz_str = (tz_idx > 0)
|
||||
.then(|| app.session.config.timezones.get(tz_idx - 1))
|
||||
.flatten();
|
||||
let role = cap.get(2).unwrap().as_str();
|
||||
|
||||
let mut label = panel
|
||||
.layout
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<WidgetLabel>(*widget_id)
|
||||
.unwrap();
|
||||
|
||||
let format = match role {
|
||||
"tz" => {
|
||||
let mut i18n = panel.layout.state.globals.i18n();
|
||||
if let Some(s) =
|
||||
tz_str.and_then(|tz| tz.split('/').next_back().map(|x| x.replace('_', " ")))
|
||||
{
|
||||
label.set_text_simple(&mut i18n, Translation::from_raw_text(&s));
|
||||
} else {
|
||||
label.set_text_simple(&mut i18n, Translation::from_raw_text("Local"));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
"date" => "%x",
|
||||
"dow" => "%A",
|
||||
"time" => {
|
||||
if app.session.config.clock_12h {
|
||||
"%I:%M %p"
|
||||
} else {
|
||||
"%H:%M"
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut i18n = panel.layout.state.globals.i18n();
|
||||
label.set_text_simple(&mut i18n, Translation::from_raw_text("ERR"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let clock = ClockState {
|
||||
timezone: tz_str.and_then(|tz| {
|
||||
tz.parse()
|
||||
.inspect_err(|e| log::warn!("Invalid timezone: {e:?}"))
|
||||
.ok()
|
||||
}),
|
||||
format: format.into(),
|
||||
};
|
||||
|
||||
panel.listeners.register(
|
||||
&mut panel.listener_handles,
|
||||
*widget_id,
|
||||
EventListenerKind::InternalStateChange,
|
||||
Box::new(move |common, data, _, _| {
|
||||
clock_on_tick(&clock, common, data);
|
||||
Ok(())
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let positioning = Positioning::FollowHand {
|
||||
hand: app.session.config.watch_hand as _,
|
||||
lerp: 1.0,
|
||||
@@ -150,22 +102,3 @@ where
|
||||
watch.state.alpha = watch.state.alpha.clamp(0., 1.);
|
||||
}
|
||||
}
|
||||
|
||||
struct ClockState {
|
||||
timezone: Option<Tz>,
|
||||
format: Rc<str>,
|
||||
}
|
||||
|
||||
fn clock_on_tick(
|
||||
clock: &ClockState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let date_time = clock.timezone.as_ref().map_or_else(
|
||||
|| format!("{}", Local::now().format(&clock.format)),
|
||||
|tz| format!("{}", Local::now().with_timezone(tz).format(&clock.format)),
|
||||
);
|
||||
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&date_time));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use glam::Affine3A;
|
||||
use idmap::IdMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::sync::Arc;
|
||||
use wgui::{
|
||||
gfx::WGfx, globals::WguiGlobals, renderer_vk::context::SharedContext as WSharedContext,
|
||||
@@ -52,15 +52,12 @@ pub struct AppState {
|
||||
pub wayvr: Option<Rc<RefCell<WayVRData>>>, // Dynamically created if requested
|
||||
}
|
||||
|
||||
#[allow(unused_mut)]
|
||||
impl AppState {
|
||||
pub fn from_graphics(gfx: Arc<WGfx>, gfx_extras: WGfxExtras) -> anyhow::Result<Self> {
|
||||
// insert shared resources
|
||||
#[cfg(feature = "wayvr")]
|
||||
let mut tasks = TaskContainer::new();
|
||||
|
||||
#[cfg(not(feature = "wayvr"))]
|
||||
let tasks = TaskContainer::new();
|
||||
|
||||
let session = AppSession::load();
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
@@ -68,6 +65,13 @@ impl AppState {
|
||||
.wayvr_config
|
||||
.post_load(&session.config, &mut tasks)?;
|
||||
|
||||
let mut hid_provider = HidWrapper::new();
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
if let Some(wayvr) = wayvr.as_ref() {
|
||||
hid_provider.set_wayvr(wayvr.clone());
|
||||
}
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
let osc_sender = crate::subsystem::osc::OscSender::new(session.config.osc_out_port).ok();
|
||||
|
||||
@@ -83,7 +87,7 @@ impl AppState {
|
||||
tasks,
|
||||
gfx,
|
||||
gfx_extras,
|
||||
hid_provider: HidWrapper::new(),
|
||||
hid_provider,
|
||||
audio_provider: AudioOutput::new(),
|
||||
wgui_shared,
|
||||
input_state: InputState::new(),
|
||||
@@ -114,6 +118,7 @@ impl AppState {
|
||||
let wayvr = Rc::new(RefCell::new(WayVRData::new(
|
||||
WayVRConfig::get_wayvr_config(&self.session.config, &self.session.wayvr_config)?,
|
||||
)?));
|
||||
self.hid_provider.set_wayvr(wayvr.clone());
|
||||
self.wayvr = Some(wayvr.clone());
|
||||
Ok(wayvr)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ impl HidWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
pub fn set_wayvr(&mut self, wayvr: Rc<RefCell<WayVRData>>) {
|
||||
self.wayvr = Some(wayvr);
|
||||
}
|
||||
|
||||
pub fn send_key_routed(&self, key: VirtualKey, down: bool) {
|
||||
match self.keyboard_focus {
|
||||
KeyboardFocus::PhysicalScreen => self.inner.send_key(key, down),
|
||||
|
||||
Reference in New Issue
Block a user