sorry about monster commit

This commit is contained in:
galister
2025-09-20 15:28:23 +09:00
parent c6a32f4109
commit cfb733de09
32 changed files with 1208 additions and 289 deletions

29
Cargo.lock generated
View File

@@ -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",

View File

@@ -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)>;

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();

View File

@@ -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);

View File

@@ -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"

View File

@@ -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>

View File

@@ -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;

View File

@@ -11,5 +11,6 @@ pub mod openxr;
pub mod wayvr;
pub mod overlay;
pub mod set;
pub mod task;

View File

@@ -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"))

View File

@@ -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"))

View File

@@ -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 },

View File

@@ -0,0 +1,9 @@
use glam::Affine3A;
use std::sync::Arc;
pub struct OverlaySetItem {
name: Arc<str>,
transform: Affine3A,
}
pub struct OverlaySet {}

View File

@@ -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]
}
}

View 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="" />
```

View 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.");
}
}
}
}

View 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: &regex::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
}
}

View 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));
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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(

View File

@@ -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.")

View File

@@ -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);

View File

@@ -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")]

View File

@@ -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))
}

View File

@@ -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));
}

View File

@@ -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)
}

View File

@@ -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),