wip: wgui backend

This commit is contained in:
galister
2025-06-18 01:06:19 +09:00
parent 2ea8b12c15
commit 95f2ae4296
50 changed files with 3066 additions and 6119 deletions

1936
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ categories = ["games"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1.0.89" anyhow = { workspace = true }
ash = "^0.38.0" # must match vulkano ash = "^0.38.0" # must match vulkano
chrono = "0.4.38" chrono = "0.4.38"
chrono-tz = "0.10.0" chrono-tz = "0.10.0"
@@ -24,17 +24,15 @@ clap = { version = "4.5.6", features = ["derive"] }
config = "0.15.11" config = "0.15.11"
ctrlc = { version = "3.4.4", features = ["termination"] } ctrlc = { version = "3.4.4", features = ["termination"] }
dbus = { version = "0.9.7" } dbus = { version = "0.9.7" }
fontconfig-rs = "0.1.1"
freetype-rs = "0.36.0" # latest version supported on ubuntu 22.04
futures = "0.3.30" futures = "0.3.30"
glam = { version = "0.30.1", features = ["approx", "mint", "serde"] } glam = { workspace = true, features = ["mint", "serde"] }
idmap = { version = "0.2.21", features = ["serde"] } idmap = { version = "0.2.21", features = ["serde"] }
idmap-derive = "0.1.2" idmap-derive = "0.1.2"
input-linux = "0.7.0" input-linux = "0.7.0"
json = { version = "0.12.4", optional = true } json = { version = "0.12.4", optional = true }
json5 = "0.4.1" json5 = "0.4.1"
libc = "0.2.155" libc = "0.2.155"
log = "0.4.21" log = { workspace = true }
openxr = { git = "https://github.com/Ralith/openxrs", rev = "d0afdd3365bc1e14de28f6a3a21f457e788a702e", features = [ openxr = { git = "https://github.com/Ralith/openxrs", rev = "d0afdd3365bc1e14de28f6a3a21f457e788a702e", features = [
"linked", "linked",
"mint", "mint",
@@ -54,14 +52,12 @@ serde_json = "1.0.117"
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
smallvec = "1.13.2" smallvec = "1.13.2"
strum = { version = "0.27.1", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] }
sysinfo = { version = "0.34.2" } sysinfo = { version = "0.35" }
thiserror = "2.0.3" thiserror = "2.0"
vulkano = { version = "0.35.1" }
vulkano-shaders = { version = "0.35.0" }
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.5.3", default-features = false } wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.5.3", default-features = false }
libmonado = { version = "1.3.2", optional = true } libmonado = { version = "1.3.2", optional = true }
winit = { version = "0.30.0", optional = true } winit = { version = "0.30", optional = true }
xdg = "2.5.2" xdg = "3.0"
log-panics = { version = "2.1.0", features = ["with-backtrace"] } log-panics = { version = "2.1.0", features = ["with-backtrace"] }
serde_json5 = "0.2.1" serde_json5 = "0.2.1"
xkbcommon = { version = "0.8.0" } xkbcommon = { version = "0.8.0" }
@@ -74,6 +70,9 @@ image_dds = { version = "0.7.2", default-features = false, features = [
mint = "0.5.9" mint = "0.5.9"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41" tracing = "0.1.41"
vulkano = { workspace = true }
vulkano-shaders = { workspace = true }
wgui = { path = "../wgui" }
################################ ################################
#WayVR-only deps #WayVR-only deps
@@ -92,6 +91,7 @@ wayland-egl = { version = "0.32.4", optional = true }
interprocess = { version = "2.2.2", optional = true } interprocess = { version = "2.2.2", optional = true }
bytes = { version = "1.9.0", optional = true } bytes = { version = "1.9.0", optional = true }
wayvr_ipc = { git = "https://github.com/olekolek1000/wayvr-ipc.git", rev = "a72587d23f3bb8624d9aeb1f13c0a21e65350f51", default-features = false, optional = true } wayvr_ipc = { git = "https://github.com/olekolek1000/wayvr-ipc.git", rev = "a72587d23f3bb8624d9aeb1f13c0a21e65350f51", default-features = false, optional = true }
rust-embed = "8.7.2"
################################ ################################
[build-dependencies] [build-dependencies]

View File

@@ -137,9 +137,8 @@ where
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] #[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
pub fn update(&mut self, app: &mut AppState) -> anyhow::Result<Vec<OverlayData<T>>> { pub fn update(&mut self, app: &mut AppState) -> anyhow::Result<Vec<OverlayData<T>>> {
use crate::overlays::{ use crate::overlays::screen::{
screen::{create_screen_interaction, create_screen_renderer_wl, load_pw_token_config}, create_screen_interaction, create_screen_renderer_wl, load_pw_token_config,
watch::create_watch_canvas,
}; };
use glam::vec2; use glam::vec2;
use wlx_capture::wayland::OutputChangeEvent; use wlx_capture::wayland::OutputChangeEvent;
@@ -256,15 +255,8 @@ where
} }
if watch_dirty { if watch_dirty {
let watch = self.mut_by_name(WATCH_NAME).unwrap(); // want panic let _watch = self.mut_by_name(WATCH_NAME).unwrap(); // want panic
match create_watch_canvas(None, app) { todo!();
Ok(canvas) => {
watch.backend = Box::new(canvas);
}
Err(e) => {
log::error!("Failed to create watch canvas: {}", e);
}
}
} }
Ok(removed_overlays) Ok(removed_overlays)

View File

@@ -11,9 +11,6 @@ pub mod openvr;
#[cfg(feature = "openxr")] #[cfg(feature = "openxr")]
pub mod openxr; pub mod openxr;
#[cfg(feature = "uidev")]
pub mod uidev;
#[cfg(feature = "osc")] #[cfg(feature = "osc")]
pub mod osc; pub mod osc;

View File

@@ -279,14 +279,14 @@ struct XsoMessage {
messageType: i32, messageType: i32,
index: Option<i32>, index: Option<i32>,
volume: Option<f32>, volume: Option<f32>,
audioPath: Option<Arc<str>>, audioPath: Option<String>,
timeout: Option<f32>, timeout: Option<f32>,
title: Arc<str>, title: String,
content: Option<Arc<str>>, content: Option<String>,
icon: Option<Arc<str>>, icon: Option<String>,
height: Option<f32>, height: Option<f32>,
opacity: Option<f32>, opacity: Option<f32>,
useBase64Icon: Option<bool>, useBase64Icon: Option<bool>,
sourceApp: Option<Arc<str>>, sourceApp: Option<String>,
alwaysShow: Option<bool>, alwaysShow: Option<bool>,
} }

View File

@@ -2,20 +2,31 @@ use std::f32::consts::PI;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use ash::vk::SubmitInfo;
use glam::{Affine3A, Vec3, Vec3A, Vec4}; use glam::{Affine3A, Vec3, Vec3A, Vec4};
use idmap::IdMap; use idmap::IdMap;
use ovr_overlay::overlay::OverlayManager; use ovr_overlay::overlay::OverlayManager;
use ovr_overlay::sys::ETrackingUniverseOrigin; use ovr_overlay::sys::ETrackingUniverseOrigin;
use vulkano::command_buffer::CommandBufferUsage; use vulkano::{
use vulkano::format::Format; command_buffer::{
use vulkano::image::view::ImageView; CommandBufferBeginInfo, CommandBufferLevel, CommandBufferUsage, RecordingCommandBuffer,
use vulkano::image::ImageLayout; },
format::Format,
image::view::ImageView,
image::{Image, ImageLayout},
sync::{
fence::{Fence, FenceCreateInfo},
AccessFlags, DependencyInfo, ImageMemoryBarrier, PipelineStages,
},
VulkanObject,
};
use wgui::gfx::WGfx;
use crate::backend::overlay::{ use crate::backend::overlay::{
FrameMeta, OverlayData, OverlayRenderer, OverlayState, ShouldRender, SplitOverlayBackend, FrameMeta, OverlayData, OverlayRenderer, OverlayState, ShouldRender, SplitOverlayBackend,
Z_ORDER_LINES, Z_ORDER_LINES,
}; };
use crate::graphics::{CommandBuffers, WlxGraphics}; use crate::graphics::CommandBuffers;
use crate::state::AppState; use crate::state::AppState;
use super::overlay::OpenVrOverlayData; use super::overlay::OpenVrOverlayData;
@@ -29,19 +40,17 @@ pub(super) struct LinePool {
} }
impl LinePool { impl LinePool {
pub fn new(graphics: Arc<WlxGraphics>) -> anyhow::Result<Self> { pub fn new(graphics: Arc<WGfx>) -> anyhow::Result<Self> {
let mut command_buffer = graphics.create_uploads_command_buffer( let mut command_buffer =
graphics.transfer_queue.clone(), graphics.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
CommandBufferUsage::OneTimeSubmit,
)?;
let buf = vec![255; 16]; let buf = vec![255; 16];
let texture = command_buffer.texture2d_raw(2, 2, Format::R8G8B8A8_UNORM, &buf)?; let texture = command_buffer.upload_image(2, 2, Format::R8G8B8A8_UNORM, &buf)?;
command_buffer.build_and_execute_now()?; command_buffer.build_and_execute_now()?;
graphics transition_layout(
.transition_layout( &graphics,
texture.clone(), texture.clone(),
ImageLayout::ShaderReadOnlyOptimal, ImageLayout::ShaderReadOnlyOptimal,
ImageLayout::TransferSrcOptimal, ImageLayout::TransferSrcOptimal,
@@ -150,7 +159,7 @@ impl LinePool {
data.after_input(overlay, app)?; data.after_input(overlay, app)?;
if data.state.want_visible { if data.state.want_visible {
if data.state.dirty { if data.state.dirty {
data.upload_texture(overlay, &app.graphics); data.upload_texture(overlay, &app.gfx);
data.state.dirty = false; data.state.dirty = false;
} }
@@ -201,3 +210,55 @@ impl OverlayRenderer for StaticRenderer {
}) })
} }
} }
pub fn transition_layout(
gfx: &WGfx,
image: Arc<Image>,
old_layout: ImageLayout,
new_layout: ImageLayout,
) -> anyhow::Result<Fence> {
let barrier = ImageMemoryBarrier {
src_stages: PipelineStages::ALL_TRANSFER,
src_access: AccessFlags::TRANSFER_WRITE,
dst_stages: PipelineStages::ALL_TRANSFER,
dst_access: AccessFlags::TRANSFER_READ,
old_layout,
new_layout,
subresource_range: image.subresource_range(),
..ImageMemoryBarrier::image(image)
};
let command_buffer = unsafe {
let mut builder = RecordingCommandBuffer::new(
gfx.command_buffer_allocator.clone(),
gfx.queue_gfx.queue_family_index(),
CommandBufferLevel::Primary,
CommandBufferBeginInfo {
usage: CommandBufferUsage::OneTimeSubmit,
inheritance_info: None,
..Default::default()
},
)?;
builder.pipeline_barrier(&DependencyInfo {
image_memory_barriers: smallvec::smallvec![barrier],
..Default::default()
})?;
builder.end()?
};
let fence = Fence::new(gfx.device.clone(), FenceCreateInfo::default())?;
let fns = gfx.device.fns();
unsafe {
(fns.v1_0.queue_submit)(
gfx.queue_gfx.handle(),
1,
[SubmitInfo::default().command_buffers(&[command_buffer.handle()])].as_ptr(),
fence.handle(),
)
}
.result()?;
Ok(fence)
}

View File

@@ -30,7 +30,7 @@ use crate::{
overlay::{OverlayData, ShouldRender}, overlay::{OverlayData, ShouldRender},
task::{SystemTask, TaskType}, task::{SystemTask, TaskType},
}, },
graphics::{CommandBuffers, WlxGraphics}, graphics::{init_openvr_graphics, CommandBuffers},
overlays::{ overlays::{
toast::{Toast, ToastTopic}, toast::{Toast, ToastTopic},
watch::{watch_fade, WATCH_NAME}, watch::{watch_fade, WATCH_NAME},
@@ -39,7 +39,7 @@ use crate::{
}; };
#[cfg(feature = "wayvr")] #[cfg(feature = "wayvr")]
use crate::{gui::modular::button::WayVRAction, overlays::wayvr::wayvr_action}; use crate::{backend::wayvr::WayVRAction, overlays::wayvr::wayvr_action};
pub mod helpers; pub mod helpers;
pub mod input; pub mod input;
@@ -95,8 +95,8 @@ pub fn openvr_run(
}; };
let mut state = { let mut state = {
let graphics = WlxGraphics::new_openvr(instance_extensions, device_extensions_fn)?; let (gfx, gfx_extras) = init_openvr_graphics(instance_extensions, device_extensions_fn)?;
AppState::from_graphics(graphics)? AppState::from_graphics(gfx, gfx_extras)?
}; };
if show_by_default { if show_by_default {
@@ -147,7 +147,7 @@ pub fn openvr_run(
let mut next_device_update = Instant::now(); let mut next_device_update = Instant::now();
let mut due_tasks = VecDeque::with_capacity(4); let mut due_tasks = VecDeque::with_capacity(4);
let mut lines = LinePool::new(state.graphics.clone())?; let mut lines = LinePool::new(state.gfx.clone())?;
let pointer_lines = [lines.allocate(), lines.allocate()]; let pointer_lines = [lines.allocate(), lines.allocate()];
'main_loop: loop { 'main_loop: loop {
@@ -361,7 +361,7 @@ pub fn openvr_run(
log::trace!("Rendering overlays"); log::trace!("Rendering overlays");
if let Some(mut future) = buffers.execute_now(state.graphics.graphics_queue.clone())? { if let Some(mut future) = buffers.execute_now(state.gfx.queue_gfx.clone())? {
if let Err(e) = future.flush() { if let Err(e) = future.flush() {
return Err(BackendError::Fatal(e.into())); return Err(BackendError::Fatal(e.into()));
} }
@@ -370,7 +370,7 @@ pub fn openvr_run(
overlays overlays
.iter_mut() .iter_mut()
.for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &state.graphics)); .for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &state.gfx));
#[cfg(feature = "wayvr")] #[cfg(feature = "wayvr")]
if let Some(wayvr) = &state.wayvr { if let Some(wayvr) = &state.wayvr {

View File

@@ -7,9 +7,13 @@ use ovr_overlay::{
pose::Matrix3x4, pose::Matrix3x4,
sys::{ETrackingUniverseOrigin, VRVulkanTextureData_t}, sys::{ETrackingUniverseOrigin, VRVulkanTextureData_t},
}; };
use vulkano::{image::view::ImageView, Handle, VulkanObject}; use vulkano::{
image::{view::ImageView, ImageUsage},
Handle, VulkanObject,
};
use wgui::gfx::WGfx;
use crate::{backend::overlay::OverlayData, graphics::WlxGraphics, state::AppState}; use crate::{backend::overlay::OverlayData, state::AppState};
use super::helpers::Affine3AConvert; use super::helpers::Affine3AConvert;
@@ -65,10 +69,11 @@ impl OverlayData<OpenVrOverlayData> {
let Some(meta) = self.backend.frame_meta() else { let Some(meta) = self.backend.frame_meta() else {
return Ok(false); return Ok(false);
}; };
let image = app.graphics.render_texture( let image = app.gfx.new_image(
meta.extent[0], meta.extent[0],
meta.extent[1], meta.extent[1],
app.graphics.native_format, app.gfx.surface_format,
ImageUsage::TRANSFER_SRC | ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED,
)?; )?;
self.data.image_view = Some(ImageView::new_default(image)?); self.data.image_view = Some(ImageView::new_default(image)?);
Ok(true) Ok(true)
@@ -91,7 +96,7 @@ impl OverlayData<OpenVrOverlayData> {
&mut self, &mut self,
universe: ETrackingUniverseOrigin, universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager, overlay: &mut OverlayManager,
graphics: &WlxGraphics, graphics: &WGfx,
) { ) {
if self.data.visible { if self.data.visible {
if self.state.dirty { if self.state.dirty {
@@ -228,7 +233,7 @@ impl OverlayData<OpenVrOverlayData> {
} }
} }
pub(super) fn upload_texture(&mut self, overlay: &mut OverlayManager, graphics: &WlxGraphics) { pub(super) fn upload_texture(&mut self, overlay: &mut OverlayManager, graphics: &WGfx) {
let Some(handle) = self.data.handle else { let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name); log::debug!("{}: No overlay handle", self.state.name);
return; return;
@@ -267,8 +272,8 @@ impl OverlayData<OpenVrOverlayData> {
m_pDevice: graphics.device.handle().as_raw() as *mut _, m_pDevice: graphics.device.handle().as_raw() as *mut _,
m_pPhysicalDevice: graphics.device.physical_device().handle().as_raw() as *mut _, m_pPhysicalDevice: graphics.device.physical_device().handle().as_raw() as *mut _,
m_pInstance: graphics.instance.handle().as_raw() as *mut _, m_pInstance: graphics.instance.handle().as_raw() as *mut _,
m_pQueue: graphics.graphics_queue.handle().as_raw() as *mut _, m_pQueue: graphics.queue_gfx.handle().as_raw() as *mut _,
m_nQueueFamilyIndex: graphics.graphics_queue.queue_family_index(), m_nQueueFamilyIndex: graphics.queue_gfx.queue_family_index(),
}; };
log::trace!( log::trace!(
"{}: UploadTex {:?}, {}x{}, {:?}", "{}: UploadTex {:?}, {}x{}, {:?}",

View File

@@ -9,11 +9,15 @@ use std::{
}, },
}; };
use vulkano::command_buffer::CommandBufferUsage; use wgui::gfx::{pipeline::WGfxPipeline, WGfx};
use crate::{ use crate::{
backend::openxr::helpers, backend::openxr::helpers,
graphics::{CommandBuffers, WlxGraphics, WlxPipeline}, graphics::{CommandBuffers, ExtentExt, Vert2Uv},
state::AppState,
};
use vulkano::{
command_buffer::CommandBufferUsage, pipeline::graphics::input_assembly::PrimitiveTopology,
}; };
use super::{ use super::{
@@ -37,20 +41,18 @@ static COLORS: [[f32; 6]; 5] = {
pub(super) struct LinePool { pub(super) struct LinePool {
lines: IdMap<usize, LineContainer>, lines: IdMap<usize, LineContainer>,
pipeline: Arc<WlxPipeline>, pipeline: Arc<WGfxPipeline<Vert2Uv>>,
} }
impl LinePool { impl LinePool {
pub(super) fn new(graphics: Arc<WlxGraphics>) -> anyhow::Result<Self> { pub(super) fn new(app: &AppState) -> anyhow::Result<Self> {
let Ok(shaders) = graphics.shared_shaders.read() else { let pipeline = app.gfx.create_pipeline(
anyhow::bail!("Failed to lock shared shaders for reading"); app.gfx_extras.shaders.get("vert_quad").unwrap().clone(), // want panic
}; app.gfx_extras.shaders.get("frag_color").unwrap().clone(), // want panic
app.gfx.surface_format,
let pipeline = graphics.create_pipeline(
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_color").unwrap().clone(), // want panic
graphics.native_format,
None, None,
PrimitiveTopology::TriangleStrip,
false,
)?; )?;
Ok(Self { Ok(Self {
@@ -59,14 +61,10 @@ impl LinePool {
}) })
} }
pub(super) fn allocate( pub(super) fn allocate(&mut self, xr: &XrState, gfx: Arc<WGfx>) -> anyhow::Result<usize> {
&mut self,
xr: &XrState,
graphics: Arc<WlxGraphics>,
) -> anyhow::Result<usize> {
let id = LINE_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed); let id = LINE_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
let srd = create_swapchain(xr, graphics, [1, 1, 1], SwapchainOpts::new())?; let srd = create_swapchain(xr, gfx, [1, 1, 1], SwapchainOpts::new())?;
self.lines.insert( self.lines.insert(
id, id,
LineContainer { LineContainer {
@@ -130,7 +128,7 @@ impl LinePool {
pub(super) fn render( pub(super) fn render(
&mut self, &mut self,
graphics: Arc<WlxGraphics>, app: &AppState,
buf: &mut CommandBuffers, buf: &mut CommandBuffers,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
for line in self.lines.values_mut() { for line in self.lines.values_mut() {
@@ -139,14 +137,19 @@ impl LinePool {
let set0 = self let set0 = self
.pipeline .pipeline
.uniform_buffer(0, COLORS[inner.color].to_vec())?; .uniform_buffer_upload(0, COLORS[inner.color].to_vec())?;
let pass = self let pass = self.pipeline.create_pass(
.pipeline tgt.extent_f32(),
.create_pass_for_target(tgt.clone(), vec![set0])?; app.gfx_extras.quad_verts.clone(),
0..4,
0..1,
vec![set0],
)?;
let mut cmd_buffer = let mut cmd_buffer = app
graphics.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?; .gfx
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_rendering(tgt)?; cmd_buffer.begin_rendering(tgt)?;
cmd_buffer.run_ref(&pass)?; cmd_buffer.run_ref(&pass)?;
cmd_buffer.end_rendering()?; cmd_buffer.end_rendering()?;

View File

@@ -24,7 +24,7 @@ use crate::{
overlay::{OverlayData, ShouldRender}, overlay::{OverlayData, ShouldRender},
task::{SystemTask, TaskType}, task::{SystemTask, TaskType},
}, },
graphics::{CommandBuffers, WlxGraphics}, graphics::{init_openxr_graphics, CommandBuffers},
overlays::{ overlays::{
toast::{Toast, ToastTopic}, toast::{Toast, ToastTopic},
watch::{watch_fade, WATCH_NAME}, watch::{watch_fade, WATCH_NAME},
@@ -33,7 +33,7 @@ use crate::{
}; };
#[cfg(feature = "wayvr")] #[cfg(feature = "wayvr")]
use crate::{gui::modular::button::WayVRAction, overlays::wayvr::wayvr_action}; use crate::{backend::wayvr::WayVRAction, overlays::wayvr::wayvr_action};
mod blocker; mod blocker;
mod helpers; mod helpers;
@@ -71,8 +71,8 @@ pub fn openxr_run(
}; };
let mut app = { let mut app = {
let graphics = WlxGraphics::new_openxr(xr_instance.clone(), system)?; let (gfx, gfx_extras) = init_openxr_graphics(xr_instance.clone(), system)?;
AppState::from_graphics(graphics)? AppState::from_graphics(gfx, gfx_extras)?
}; };
let environment_blend_mode = { let environment_blend_mode = {
@@ -95,7 +95,7 @@ pub fn openxr_run(
} }
let mut overlays = OverlayContainer::<OpenXrOverlayData>::new(&mut app, headless)?; let mut overlays = OverlayContainer::<OpenXrOverlayData>::new(&mut app, headless)?;
let mut lines = LinePool::new(app.graphics.clone())?; let mut lines = LinePool::new(&app)?;
let mut notifications = NotificationManager::new(); let mut notifications = NotificationManager::new();
notifications.run_dbus(); notifications.run_dbus();
@@ -120,10 +120,10 @@ pub fn openxr_run(
&xr_instance, &xr_instance,
system, system,
&xr::vulkan::SessionCreateInfo { &xr::vulkan::SessionCreateInfo {
instance: app.graphics.instance.handle().as_raw() as _, instance: app.gfx.instance.handle().as_raw() as _,
physical_device: app.graphics.device.physical_device().handle().as_raw() as _, physical_device: app.gfx.device.physical_device().handle().as_raw() as _,
device: app.graphics.device.handle().as_raw() as _, device: app.gfx.device.handle().as_raw() as _,
queue_family_index: app.graphics.graphics_queue.queue_family_index(), queue_family_index: app.gfx.queue_gfx.queue_family_index(),
queue_index: 0, queue_index: 0,
}, },
)?; )?;
@@ -151,8 +151,8 @@ pub fn openxr_run(
}; };
let pointer_lines = [ let pointer_lines = [
lines.allocate(&xr_state, app.graphics.clone())?, lines.allocate(&xr_state, app.gfx.clone())?,
lines.allocate(&xr_state, app.graphics.clone())?, lines.allocate(&xr_state, app.gfx.clone())?,
]; ];
let watch_id = overlays.get_by_name(WATCH_NAME).unwrap().state.id; // want panic let watch_id = overlays.get_by_name(WATCH_NAME).unwrap().state.id; // want panic
@@ -418,9 +418,9 @@ pub fn openxr_run(
o.data.cur_visible = true; o.data.cur_visible = true;
} }
lines.render(app.graphics.clone(), &mut buffers)?; lines.render(&app, &mut buffers)?;
let future = buffers.execute_now(app.graphics.graphics_queue.clone())?; let future = buffers.execute_now(app.gfx.queue_gfx.clone())?;
if let Some(mut future) = future { if let Some(mut future) = future {
if let Err(e) = future.flush() { if let Err(e) = future.flush() {
return Err(BackendError::Fatal(e.into())); return Err(BackendError::Fatal(e.into()));

View File

@@ -42,7 +42,7 @@ impl OverlayData<OpenXrOverlayData> {
let extent = meta.extent; let extent = meta.extent;
self.data.swapchain = Some(create_swapchain( self.data.swapchain = Some(create_swapchain(
xr, xr,
app.graphics.clone(), app.gfx.clone(),
extent, extent,
SwapchainOpts::new(), SwapchainOpts::new(),
)?); )?);

View File

@@ -5,16 +5,17 @@ use std::{
}; };
use glam::{Quat, Vec3A}; use glam::{Quat, Vec3A};
use openxr::{self as xr, CompositionLayerFlags}; use openxr as xr;
use vulkano::{ use vulkano::{
command_buffer::CommandBufferUsage, image::view::ImageView, command_buffer::CommandBufferUsage,
pipeline::graphics::color_blend::AttachmentBlend, image::view::ImageView,
pipeline::graphics::{color_blend::AttachmentBlend, input_assembly::PrimitiveTopology},
}; };
use crate::{ use crate::{
backend::openxr::{helpers::translation_rotation_to_posef, swapchain::SwapchainOpts}, backend::openxr::{helpers::translation_rotation_to_posef, swapchain::SwapchainOpts},
config_io, config_io,
graphics::{dds::WlxCommandBufferDds, CommandBuffers}, graphics::{dds::WlxCommandBufferDds, CommandBuffers, ExtentExt},
state::AppState, state::AppState,
}; };
@@ -31,10 +32,9 @@ pub(super) struct Skybox {
impl Skybox { impl Skybox {
pub fn new(app: &AppState) -> anyhow::Result<Self> { pub fn new(app: &AppState) -> anyhow::Result<Self> {
let mut command_buffer = app.graphics.create_uploads_command_buffer( let mut command_buffer = app
app.graphics.transfer_queue.clone(), .gfx
CommandBufferUsage::OneTimeSubmit, .create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
)?;
let mut maybe_image = None; let mut maybe_image = None;
@@ -51,7 +51,7 @@ impl Skybox {
); );
break 'custom_tex; break 'custom_tex;
}; };
match command_buffer.texture2d_dds(f) { match command_buffer.upload_image_dds(f) {
Ok(image) => { Ok(image) => {
maybe_image = Some(image); maybe_image = Some(image);
} }
@@ -67,7 +67,7 @@ impl Skybox {
if maybe_image.is_none() { if maybe_image.is_none() {
let p = include_bytes!("../../res/table_mountain_2.dds"); let p = include_bytes!("../../res/table_mountain_2.dds");
maybe_image = Some(command_buffer.texture2d_dds(p.as_slice())?); maybe_image = Some(command_buffer.upload_image_dds(p.as_slice())?);
} }
command_buffer.build_and_execute_now()?; command_buffer.build_and_execute_now()?;
@@ -92,30 +92,31 @@ impl Skybox {
} }
let opts = SwapchainOpts::new().immutable(); let opts = SwapchainOpts::new().immutable();
let Ok(shaders) = app.graphics.shared_shaders.read() else {
anyhow::bail!("Failed to lock shared shaders for reading");
};
let extent = self.view.image().extent(); let extent = self.view.image().extent();
let mut swapchain = create_swapchain(xr, app.graphics.clone(), extent, opts)?; let mut swapchain = create_swapchain(xr, app.gfx.clone(), extent, opts)?;
let tgt = swapchain.acquire_wait_image()?; let tgt = swapchain.acquire_wait_image()?;
let pipeline = app.graphics.create_pipeline( let pipeline = app.gfx.create_pipeline(
shaders.get("vert_common").unwrap().clone(), // want panic app.gfx_extras.shaders.get("vert_quad").unwrap().clone(), // want panic
shaders.get("frag_srgb").unwrap().clone(), // want panic app.gfx_extras.shaders.get("frag_srgb").unwrap().clone(), // want panic
app.graphics.native_format, app.gfx.surface_format,
None, None,
PrimitiveTopology::TriangleStrip,
false,
)?; )?;
let set0 = let set0 = pipeline.uniform_sampler(0, self.view.clone(), app.gfx.texture_filter)?;
pipeline.uniform_sampler(0, self.view.clone(), app.graphics.texture_filtering)?; let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?;
let pass = pipeline.create_pass(
let set1 = pipeline.uniform_buffer(1, vec![1f32])?; tgt.extent_f32(),
app.gfx_extras.quad_verts.clone(),
let pass = pipeline.create_pass_for_target(tgt.clone(), vec![set0, set1])?; 0..4,
0..1,
vec![set0, set1],
)?;
let mut cmd_buffer = app let mut cmd_buffer = app
.graphics .gfx
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?; .create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_rendering(tgt)?; cmd_buffer.begin_rendering(tgt)?;
cmd_buffer.run_ref(&pass)?; cmd_buffer.run_ref(&pass)?;
cmd_buffer.end_rendering()?; cmd_buffer.end_rendering()?;
@@ -135,30 +136,35 @@ impl Skybox {
if self.grid.is_some() { if self.grid.is_some() {
return Ok(()); return Ok(());
} }
let Ok(shaders) = app.graphics.shared_shaders.read() else {
anyhow::bail!("Failed to lock shared shaders for reading");
};
let extent = [1024, 1024, 1]; let extent = [1024, 1024, 1];
let mut swapchain = create_swapchain( let mut swapchain = create_swapchain(
xr, xr,
app.graphics.clone(), app.gfx.clone(),
extent, extent,
SwapchainOpts::new().immutable(), SwapchainOpts::new().immutable(),
)?; )?;
let pipeline = app.graphics.create_pipeline( let pipeline = app.gfx.create_pipeline(
shaders.get("vert_common").unwrap().clone(), // want panic app.gfx_extras.shaders.get("vert_quad").unwrap().clone(), // want panic
shaders.get("frag_grid").unwrap().clone(), // want panic app.gfx_extras.shaders.get("frag_grid").unwrap().clone(), // want panic
app.graphics.native_format, app.gfx.surface_format,
Some(AttachmentBlend::alpha()), Some(AttachmentBlend::alpha()),
PrimitiveTopology::TriangleStrip,
false,
)?; )?;
let tgt = swapchain.acquire_wait_image()?; let tgt = swapchain.acquire_wait_image()?;
let pass = pipeline.create_pass_for_target(tgt.clone(), vec![])?; let pass = pipeline.create_pass(
tgt.extent_f32(),
app.gfx_extras.quad_verts.clone(),
0..4,
0..1,
vec![],
)?;
let mut cmd_buffer = app let mut cmd_buffer = app
.graphics .gfx
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?; .create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_rendering(tgt)?; cmd_buffer.begin_rendering(tgt)?;
cmd_buffer.run_ref(&pass)?; cmd_buffer.run_ref(&pass)?;
cmd_buffer.end_rendering()?; cmd_buffer.end_rendering()?;
@@ -206,7 +212,7 @@ impl Skybox {
self.sky.as_mut().unwrap().ensure_image_released()?; self.sky.as_mut().unwrap().ensure_image_released()?;
let sky = xr::CompositionLayerEquirect2KHR::new() let sky = xr::CompositionLayerEquirect2KHR::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA) .layer_flags(xr::CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(pose) .pose(pose)
.radius(10.0) .radius(10.0)
.sub_image(self.sky.as_ref().unwrap().get_subimage()) .sub_image(self.sky.as_ref().unwrap().get_subimage())
@@ -218,7 +224,7 @@ impl Skybox {
self.grid.as_mut().unwrap().ensure_image_released()?; self.grid.as_mut().unwrap().ensure_image_released()?;
let grid = xr::CompositionLayerQuad::new() let grid = xr::CompositionLayerQuad::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA) .layer_flags(xr::CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(*GRID_POSE) .pose(*GRID_POSE)
.size(xr::Extent2Df { .size(xr::Extent2Df {
width: 10.0, width: 10.0,

View File

@@ -8,8 +8,7 @@ use vulkano::{
image::{sys::RawImage, view::ImageView, ImageCreateInfo, ImageUsage}, image::{sys::RawImage, view::ImageView, ImageCreateInfo, ImageUsage},
Handle, Handle,
}; };
use wgui::gfx::WGfx;
use crate::graphics::WlxGraphics;
use super::XrState; use super::XrState;
@@ -30,7 +29,7 @@ impl SwapchainOpts {
pub(super) fn create_swapchain( pub(super) fn create_swapchain(
xr: &XrState, xr: &XrState,
graphics: Arc<WlxGraphics>, gfx: Arc<WGfx>,
extent: [u32; 3], extent: [u32; 3],
opts: SwapchainOpts, opts: SwapchainOpts,
) -> anyhow::Result<WlxSwapchain> { ) -> anyhow::Result<WlxSwapchain> {
@@ -43,7 +42,7 @@ pub(super) fn create_swapchain(
let swapchain = xr.session.create_swapchain(&xr::SwapchainCreateInfo { let swapchain = xr.session.create_swapchain(&xr::SwapchainCreateInfo {
create_flags, create_flags,
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT | xr::SwapchainUsageFlags::SAMPLED, usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT | xr::SwapchainUsageFlags::SAMPLED,
format: graphics.native_format as _, format: gfx.surface_format as _,
sample_count: 1, sample_count: 1,
width: extent[0], width: extent[0],
height: extent[1], height: extent[1],
@@ -60,10 +59,10 @@ pub(super) fn create_swapchain(
// thanks @yshui // thanks @yshui
let raw_image = unsafe { let raw_image = unsafe {
RawImage::from_handle_borrowed( RawImage::from_handle_borrowed(
graphics.device.clone(), gfx.device.clone(),
vk_image, vk_image,
ImageCreateInfo { ImageCreateInfo {
format: graphics.native_format as _, format: gfx.surface_format as _,
extent, extent,
usage: ImageUsage::COLOR_ATTACHMENT, usage: ImageUsage::COLOR_ATTACHMENT,
..Default::default() ..Default::default()

View File

@@ -301,8 +301,11 @@ pub struct FrameMeta {
} }
pub enum ShouldRender { pub enum ShouldRender {
/// The overlay is dirty and needs to be rendered.
Should, Should,
/// The overlay is not dirty but is ready to be rendered.
Can, Can,
/// The overlay is not ready to be rendered.
Unable, Unable,
} }

View File

@@ -10,7 +10,7 @@ use serde::Deserialize;
use crate::state::AppState; use crate::state::AppState;
#[cfg(feature = "wayvr")] #[cfg(feature = "wayvr")]
use crate::gui::modular::button::WayVRAction; use crate::backend::wayvr::WayVRAction;
use super::{ use super::{
common::OverlaySelector, common::OverlaySelector,

View File

@@ -1,289 +0,0 @@
use std::sync::Arc;
use vulkano::{
image::{view::ImageView, ImageUsage},
swapchain::{
acquire_next_image, Surface, SurfaceInfo, Swapchain, SwapchainCreateInfo,
SwapchainPresentInfo,
},
sync::GpuFuture,
Validated, VulkanError,
};
use winit::{
dpi::LogicalSize,
event::{Event, WindowEvent},
event_loop::ControlFlow,
window::Window,
};
use crate::{
config::load_custom_ui,
config_io,
graphics::{CommandBuffers, WlxGraphics},
gui::{
canvas::Canvas,
modular::{modular_canvas, ModularData},
},
hid::USE_UINPUT,
state::{AppState, ScreenMeta},
};
use super::{
input::{TrackedDevice, TrackedDeviceRole},
overlay::{OverlayID, OverlayRenderer},
};
static LAST_SIZE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
struct PreviewState {
canvas: Canvas<(), ModularData>,
swapchain: Arc<Swapchain>,
images: Vec<Arc<ImageView>>,
}
impl PreviewState {
fn new(
state: &mut AppState,
surface: Arc<Surface>,
window: Arc<Window>,
panel_name: &str,
) -> anyhow::Result<Self> {
let config = load_custom_ui(panel_name)?;
let last_size = {
let size_u64 = LAST_SIZE.load(std::sync::atomic::Ordering::Relaxed);
[size_u64 as u32, (size_u64 >> 32) as u32]
};
if last_size != config.size {
let logical_size = LogicalSize::new(config.size[0], config.size[1]);
let _ = window.request_inner_size(logical_size);
window.set_min_inner_size(Some(logical_size));
window.set_max_inner_size(Some(logical_size));
LAST_SIZE.store(
((config.size[1] as u64) << 32) | config.size[0] as u64,
std::sync::atomic::Ordering::Relaxed,
);
}
let inner_size = window.inner_size();
let swapchain_size = [inner_size.width, inner_size.height];
let (swapchain, images) = create_swapchain(&state.graphics, surface, swapchain_size)?;
let mut canvas = modular_canvas(config.size, &config.elements, state)?;
canvas.init(state)?;
Ok(Self {
canvas,
swapchain,
images,
})
}
}
#[allow(clippy::too_many_lines)]
pub fn uidev_run(panel_name: &str) -> anyhow::Result<()> {
let (graphics, event_loop, window, surface) = WlxGraphics::new_window()?;
window.set_resizable(false);
window.set_title("WlxOverlay UI Preview");
USE_UINPUT.store(false, std::sync::atomic::Ordering::Relaxed);
let mut state = AppState::from_graphics(graphics.clone())?;
add_dummy_devices(&mut state);
add_dummy_screens(&mut state);
let mut preview = Some(PreviewState::new(
&mut state,
surface.clone(),
window.clone(),
panel_name,
)?);
let watch_path = config_io::get_config_root().join(format!("{panel_name}.yaml"));
let mut path_last_modified = watch_path.metadata()?.modified()?;
let mut recreate = false;
let mut last_draw = std::time::Instant::now();
#[allow(deprecated)]
event_loop.run(move |event, elwt| {
elwt.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
elwt.exit();
}
Event::WindowEvent {
event: WindowEvent::Resized(_),
..
} => {
recreate = true;
}
Event::WindowEvent {
event: WindowEvent::RedrawRequested,
..
} => {
let new_modified = watch_path.metadata().unwrap().modified().unwrap();
if new_modified > path_last_modified {
recreate = true;
path_last_modified = new_modified;
}
if recreate {
drop(preview.take());
preview = Some(
PreviewState::new(&mut state, surface.clone(), window.clone(), panel_name)
.unwrap(),
);
recreate = false;
window.request_redraw();
}
{
let preview = preview.as_mut().unwrap();
let (image_index, _, acquire_future) =
match acquire_next_image(preview.swapchain.clone(), None)
.map_err(Validated::unwrap)
{
Ok(r) => r,
Err(VulkanError::OutOfDate) => {
recreate = true;
return;
}
Err(e) => panic!("failed to acquire next image: {e}"),
};
let mut canvas_cmd_buf = CommandBuffers::default();
let tgt = preview.images[image_index as usize].clone();
if let Err(e) = preview
.canvas
.render(&mut state, tgt, &mut canvas_cmd_buf, 1.0)
{
log::error!("failed to render canvas: {e}");
window.request_redraw();
}
last_draw = std::time::Instant::now();
canvas_cmd_buf
.execute_after(
state.graphics.graphics_queue.clone(),
Box::new(acquire_future),
)
.unwrap()
.then_swapchain_present(
graphics.graphics_queue.clone(),
SwapchainPresentInfo::swapchain_image_index(
preview.swapchain.clone(),
image_index,
),
)
.then_signal_fence_and_flush()
.unwrap()
.wait(None)
.unwrap();
}
}
Event::AboutToWait => {
if last_draw.elapsed().as_millis() > 100 {
window.request_redraw();
}
}
_ => (),
}
})?;
Ok(())
}
fn create_swapchain(
graphics: &WlxGraphics,
surface: Arc<Surface>,
extent: [u32; 2],
) -> anyhow::Result<(Arc<Swapchain>, Vec<Arc<ImageView>>)> {
let surface_capabilities = graphics
.device
.physical_device()
.surface_capabilities(&surface, SurfaceInfo::default())
.unwrap(); // want panic
let (swapchain, images) = Swapchain::new(
graphics.device.clone(),
surface,
SwapchainCreateInfo {
min_image_count: surface_capabilities.min_image_count.max(2),
image_format: graphics.native_format,
image_extent: extent,
image_usage: ImageUsage::COLOR_ATTACHMENT,
composite_alpha: surface_capabilities
.supported_composite_alpha
.into_iter()
.next()
.unwrap(), // want panic
..Default::default()
},
)?;
let image_views = images
.into_iter()
// want panic
.map(|image| ImageView::new_default(image).unwrap())
.collect::<Vec<_>>();
Ok((swapchain, image_views))
}
fn add_dummy_devices(app: &mut AppState) {
app.input_state.devices.push(TrackedDevice {
role: TrackedDeviceRole::Hmd,
soc: Some(0.42),
charging: true,
});
app.input_state.devices.push(TrackedDevice {
role: TrackedDeviceRole::LeftHand,
soc: Some(0.72),
charging: false,
});
app.input_state.devices.push(TrackedDevice {
role: TrackedDeviceRole::RightHand,
soc: Some(0.73),
charging: false,
});
app.input_state.devices.push(TrackedDevice {
role: TrackedDeviceRole::Tracker,
soc: Some(0.65),
charging: false,
});
app.input_state.devices.push(TrackedDevice {
role: TrackedDeviceRole::Tracker,
soc: Some(0.67),
charging: false,
});
app.input_state.devices.push(TrackedDevice {
role: TrackedDeviceRole::Tracker,
soc: Some(0.69),
charging: false,
});
}
fn add_dummy_screens(app: &mut AppState) {
app.screens.push(ScreenMeta {
name: "HDMI-A-1".into(),
id: OverlayID(0),
native_handle: 0,
});
app.screens.push(ScreenMeta {
name: "DP-2".into(),
id: OverlayID(0),
native_handle: 0,
});
app.screens.push(ScreenMeta {
name: "DP-3".into(),
id: OverlayID(0),
native_handle: 0,
});
}

View File

@@ -14,6 +14,7 @@ use comp::Application;
use display::{Display, DisplayInitParams, DisplayVec}; use display::{Display, DisplayInitParams, DisplayVec};
use event_queue::SyncEventQueue; use event_queue::SyncEventQueue;
use process::ProcessVec; use process::ProcessVec;
use serde::Deserialize;
use server_ipc::WayVRServer; use server_ipc::WayVRServer;
use smallvec::SmallVec; use smallvec::SmallVec;
use smithay::{ use smithay::{
@@ -36,6 +37,7 @@ use std::{
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
rc::Rc, rc::Rc,
sync::Arc,
}; };
use time::get_millis; use time::get_millis;
use wayvr_ipc::{packet_client, packet_server}; use wayvr_ipc::{packet_client, packet_server};
@@ -717,3 +719,22 @@ impl WayVRState {
Ok(handle) Ok(handle)
} }
} }
#[derive(Deserialize, Clone)]
pub enum WayVRDisplayClickAction {
ToggleVisibility,
Reset,
}
#[derive(Deserialize, Clone)]
pub enum WayVRAction {
AppClick {
catalog_name: Arc<str>,
app_name: Arc<str>,
},
DisplayClick {
display_name: Arc<str>,
action: WayVRDisplayClickAction,
},
ToggleDashboard,
}

View File

@@ -2,11 +2,9 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use crate::config_io; use crate::config_io;
use crate::gui::modular::ModularUiConfig;
use crate::overlays::toast::DisplayMethod; use crate::overlays::toast::DisplayMethod;
use crate::overlays::toast::ToastTopic; use crate::overlays::toast::ToastTopic;
use crate::state::LeftRight; use crate::state::LeftRight;
use anyhow::bail;
use chrono::Offset; use chrono::Offset;
use config::Config; use config::Config;
use config::File; use config::File;
@@ -384,14 +382,6 @@ where
panic!("No usable config found."); panic!("No usable config found.");
} }
pub fn load_custom_ui(name: &str) -> anyhow::Result<ModularUiConfig> {
let filename = format!("{name}.yaml");
let Some(yaml_data) = config_io::load(&filename) else {
bail!("Could not read file at {}", &filename);
};
Ok(serde_yaml::from_str(&yaml_data)?)
}
pub fn load_config_with_conf_d<ConfigData>( pub fn load_config_with_conf_d<ConfigData>(
root_config_filename: &str, root_config_filename: &str,
ctype: config_io::ConfigRoot, ctype: config_io::ConfigRoot,

View File

@@ -10,8 +10,7 @@ pub enum ConfigRoot {
const FALLBACK_CONFIG_PATH: &str = "/tmp/wlxoverlay"; const FALLBACK_CONFIG_PATH: &str = "/tmp/wlxoverlay";
static CONFIG_ROOT_PATH: LazyLock<PathBuf> = LazyLock::new(|| { static CONFIG_ROOT_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
if let Ok(xdg_dirs) = xdg::BaseDirectories::new() { if let Some(mut dir) = xdg::BaseDirectories::new().get_config_home() {
let mut dir = xdg_dirs.get_config_home();
dir.push("wlxoverlay"); dir.push("wlxoverlay");
return dir; return dir;
} }

View File

@@ -14,11 +14,10 @@ use crate::{
backend::{ backend::{
overlay::Positioning, overlay::Positioning,
task::{TaskContainer, TaskType}, task::{TaskContainer, TaskType},
wayvr, wayvr::{self, WayVRAction},
}, },
config::load_config_with_conf_d, config::load_config_with_conf_d,
config_io, config_io,
gui::modular::button::WayVRAction,
overlays::wayvr::{executable_exists_in_path, WayVRData}, overlays::wayvr::{executable_exists_in_path, WayVRData},
}; };

View File

@@ -8,17 +8,16 @@ use vulkano::{
memory::allocator::{AllocationCreateInfo, MemoryTypeFilter}, memory::allocator::{AllocationCreateInfo, MemoryTypeFilter},
DeviceSize, DeviceSize,
}; };
use wgui::gfx::cmd::XferCommandBuffer;
use super::WlxUploadsBuffer;
pub trait WlxCommandBufferDds { pub trait WlxCommandBufferDds {
fn texture2d_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>> fn upload_image_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>>
where where
R: Read; R: Read;
} }
impl WlxCommandBufferDds for WlxUploadsBuffer { impl WlxCommandBufferDds for XferCommandBuffer {
fn texture2d_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>> fn upload_image_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>>
where where
R: Read, R: Read,
{ {

View File

@@ -1,12 +1,142 @@
use std::{mem::MaybeUninit, sync::Arc}; use std::{
mem::MaybeUninit,
os::fd::{FromRawFd, IntoRawFd},
sync::Arc,
};
use smallvec::SmallVec; use smallvec::SmallVec;
use vulkano::{ use vulkano::{
device::Device, device::Device,
image::{sys::RawImage, ImageCreateInfo, SubresourceLayout}, format::Format,
image::{sys::RawImage, Image, ImageCreateInfo, ImageTiling, ImageUsage, SubresourceLayout},
memory::{
allocator::{MemoryAllocator, MemoryTypeFilter},
DedicatedAllocation, DeviceMemory, ExternalMemoryHandleType, ExternalMemoryHandleTypes,
MemoryAllocateInfo, MemoryImportInfo, MemoryPropertyFlags, ResourceMemory,
},
sync::Sharing, sync::Sharing,
VulkanError, VulkanObject, VulkanError, VulkanObject,
}; };
use wgui::gfx::WGfx;
use wlx_capture::frame::{
DmabufFrame, DrmFormat, FourCC, DRM_FORMAT_ABGR2101010, DRM_FORMAT_ABGR8888,
DRM_FORMAT_ARGB8888, DRM_FORMAT_XBGR2101010, DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888,
};
pub const DRM_FORMAT_MOD_INVALID: u64 = 0xff_ffff_ffff_ffff;
pub trait WGfxDmabuf {
fn dmabuf_texture_ex(
&self,
frame: DmabufFrame,
tiling: ImageTiling,
layouts: Vec<SubresourceLayout>,
modifiers: &[u64],
) -> anyhow::Result<Arc<Image>>;
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>>;
}
impl WGfxDmabuf for WGfx {
fn dmabuf_texture_ex(
&self,
frame: DmabufFrame,
tiling: ImageTiling,
layouts: Vec<SubresourceLayout>,
modifiers: &[u64],
) -> anyhow::Result<Arc<Image>> {
let extent = [frame.format.width, frame.format.height, 1];
let format = fourcc_to_vk(frame.format.fourcc)?;
let image = unsafe {
create_dmabuf_image(
self.device.clone(),
ImageCreateInfo {
format,
extent,
usage: ImageUsage::SAMPLED,
external_memory_handle_types: ExternalMemoryHandleTypes::DMA_BUF,
tiling,
drm_format_modifiers: modifiers.to_owned(),
drm_format_modifier_plane_layouts: layouts,
..Default::default()
},
)?
};
let requirements = image.memory_requirements()[0];
let memory_type_index = self
.memory_allocator
.find_memory_type_index(
requirements.memory_type_bits,
MemoryTypeFilter {
required_flags: MemoryPropertyFlags::DEVICE_LOCAL,
..Default::default()
},
)
.ok_or_else(|| anyhow::anyhow!("failed to get memory type index"))?;
debug_assert!(self.device.enabled_extensions().khr_external_memory_fd);
debug_assert!(self.device.enabled_extensions().khr_external_memory);
debug_assert!(self.device.enabled_extensions().ext_external_memory_dma_buf);
// only do the 1st
unsafe {
let Some(fd) = frame.planes[0].fd else {
anyhow::bail!("DMA-buf plane has no FD");
};
let file = std::fs::File::from_raw_fd(fd);
let new_file = file.try_clone()?;
let _ = file.into_raw_fd();
let memory = DeviceMemory::allocate_unchecked(
self.device.clone(),
MemoryAllocateInfo {
allocation_size: requirements.layout.size(),
memory_type_index,
dedicated_allocation: Some(DedicatedAllocation::Image(&image)),
..Default::default()
},
Some(MemoryImportInfo::Fd {
file: new_file,
handle_type: ExternalMemoryHandleType::DmaBuf,
}),
)?;
let mem_alloc = ResourceMemory::new_dedicated(memory);
match image.bind_memory_unchecked([mem_alloc]) {
Ok(image) => Ok(Arc::new(image)),
Err(e) => {
anyhow::bail!("Failed to bind memory to image: {}", e.0);
}
}
}
}
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>> {
let mut modifiers: Vec<u64> = vec![];
let mut tiling: ImageTiling = ImageTiling::Optimal;
let mut layouts: Vec<SubresourceLayout> = vec![];
if frame.format.modifier != DRM_FORMAT_MOD_INVALID {
(0..frame.num_planes).for_each(|i| {
let plane = &frame.planes[i];
layouts.push(SubresourceLayout {
offset: plane.offset.into(),
size: 0,
row_pitch: plane.stride as _,
array_pitch: None,
depth_pitch: None,
});
modifiers.push(frame.format.modifier);
});
tiling = ImageTiling::DrmFormatModifier;
}
self.dmabuf_texture_ex(frame, tiling, layouts, &modifiers)
}
}
#[allow(clippy::all, clippy::pedantic)] #[allow(clippy::all, clippy::pedantic)]
pub(super) unsafe fn create_dmabuf_image( pub(super) unsafe fn create_dmabuf_image(
@@ -170,3 +300,51 @@ pub(super) unsafe fn create_dmabuf_image(
RawImage::from_handle(device, handle, create_info) RawImage::from_handle(device, handle, create_info)
} }
pub fn get_drm_formats(device: Arc<Device>) -> Vec<DrmFormat> {
let possible_formats = [
DRM_FORMAT_ABGR8888.into(),
DRM_FORMAT_XBGR8888.into(),
DRM_FORMAT_ARGB8888.into(),
DRM_FORMAT_XRGB8888.into(),
DRM_FORMAT_ABGR2101010.into(),
DRM_FORMAT_XBGR2101010.into(),
];
let mut final_formats = vec![];
for &f in &possible_formats {
let Ok(vk_fmt) = fourcc_to_vk(f) else {
continue;
};
let Ok(props) = device.physical_device().format_properties(vk_fmt) else {
continue;
};
let mut fmt = DrmFormat {
fourcc: f,
modifiers: props
.drm_format_modifier_properties
.iter()
// important bit: only allow single-plane
.filter(|m| m.drm_format_modifier_plane_count == 1)
.map(|m| m.drm_format_modifier)
.collect(),
};
fmt.modifiers.push(DRM_FORMAT_MOD_INVALID); // implicit modifiers support
final_formats.push(fmt);
}
log::debug!("Supported DRM formats:");
for f in &final_formats {
log::debug!(" {} {:?}", f.fourcc, f.modifiers);
}
final_formats
}
pub fn fourcc_to_vk(fourcc: FourCC) -> anyhow::Result<Format> {
match fourcc.value {
DRM_FORMAT_ABGR8888 | DRM_FORMAT_XBGR8888 => Ok(Format::R8G8B8A8_UNORM),
DRM_FORMAT_ARGB8888 | DRM_FORMAT_XRGB8888 => Ok(Format::B8G8R8A8_UNORM),
DRM_FORMAT_ABGR2101010 | DRM_FORMAT_XBGR2101010 => Ok(Format::A2B10G10R10_UNORM_PACK32),
_ => anyhow::bail!("Unsupported format {}", fourcc),
}
}

File diff suppressed because it is too large Load Diff

12
src/gui/asset.rs Normal file
View File

@@ -0,0 +1,12 @@
#[derive(rust_embed::Embed)]
#[folder = "src/gui/assets/"]
pub struct GuiAsset;
impl wgui::assets::AssetProvider for GuiAsset {
fn load_from_path(&mut self, path: &str) -> anyhow::Result<Vec<u8>> {
match GuiAsset::get(path) {
Some(data) => Ok(data.data.to_vec()),
None => anyhow::bail!("embedded file {} not found", path),
}
}
}

0
src/gui/assets/test Normal file
View File

View File

@@ -1,267 +0,0 @@
use glam::Vec4;
use std::sync::Arc;
use vulkano::format::Format;
use crate::{
graphics::WlxGraphics,
gui::{canvas::control::ControlRenderer, GuiColor, KeyCapType},
};
use super::{control::Control, Canvas, Rect};
pub struct CanvasBuilder<D, S> {
canvas: Canvas<D, S>,
pub fg_color: GuiColor,
pub bg_color: GuiColor,
pub font_size: isize,
}
impl<D, S> CanvasBuilder<D, S> {
pub fn new(
width: usize,
height: usize,
graphics: Arc<WlxGraphics>,
format: Format,
data: D,
) -> anyhow::Result<Self> {
Ok(Self {
canvas: Canvas::new(width, height, graphics, format, data)?,
bg_color: Vec4::ZERO,
fg_color: Vec4::ONE,
font_size: 16,
})
}
pub fn build(self) -> Canvas<D, S> {
self.canvas
}
// Creates a panel with bg_color inherited from the canvas
pub fn panel(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
bg_color: self.bg_color,
on_render_bg: Some(Control::render_rounded_rect),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a label with fg_color, font_size inherited from the canvas
pub fn label(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
text: Arc<str>,
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
text,
fg_color: self.fg_color,
size: self.font_size,
on_render_fg: Some(Control::render_text),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a label with fg_color, font_size inherited from the canvas
#[allow(dead_code)]
pub fn label_centered(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
text: Arc<str>,
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
text,
fg_color: self.fg_color,
size: self.font_size,
on_render_fg: Some(Control::render_text_centered),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a sprite. Will not draw anything until set_sprite is called.
pub fn sprite(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: 0.,
on_render_bg: Some(Control::render_sprite_bg),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a sprite that highlights on pointer hover. Will not draw anything until set_sprite is called.
#[allow(dead_code)]
pub fn sprite_interactive(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: 0.,
on_render_bg: Some(Control::render_sprite_bg),
on_render_hl: Some(Control::render_sprite_hl),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a button with fg_color, bg_color, font_size inherited from the canvas
pub fn button(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
text: Arc<str>,
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.interactive_set_idx(x, y, w, h, idx);
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
text,
fg_color: self.fg_color,
bg_color: self.bg_color,
size: self.font_size,
on_render_bg: Some(Control::render_rounded_rect),
on_render_fg: Some(Control::render_text_centered),
on_render_hl: Some(Control::render_highlight),
..Control::new()
});
&mut self.canvas.controls[idx]
}
#[allow(clippy::too_many_arguments)]
pub fn key_button(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
cap_type: KeyCapType,
label: &[String],
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.interactive_set_idx(x, y, w, h, idx);
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
bg_color: self.bg_color,
on_render_bg: Some(Control::render_rounded_rect),
on_render_hl: Some(Control::render_highlight),
..Control::new()
});
let renders = match cap_type {
KeyCapType::Regular => {
let render: ControlRenderer<D, S> = Control::render_text_centered;
let rect = Rect {
x,
y,
w,
h: h - self.font_size as f32,
};
vec![(render, rect, 1f32)]
}
KeyCapType::RegularAltGr => {
let render: ControlRenderer<D, S> = Control::render_text;
let rect0 = Rect {
x: x + 12.,
y: y + (self.font_size as f32) + 12.,
w,
h,
};
let rect1 = Rect {
x: w.mul_add(0.5, x) + 12.,
y: y + h - (self.font_size as f32) + 8.,
w,
h,
};
vec![(render, rect0, 1.0), (render, rect1, 0.8)]
}
KeyCapType::Reversed => {
let render: ControlRenderer<D, S> = Control::render_text_centered;
let rect0 = Rect {
x,
y: y + 2.0,
w,
h: h * 0.5,
};
let rect1 = Rect {
x,
y: h.mul_add(0.5, y) + 2.0,
w,
h: h * 0.5,
};
vec![(render, rect1, 1.0), (render, rect0, 0.8)]
}
KeyCapType::ReversedAltGr => {
let render: ControlRenderer<D, S> = Control::render_text;
let rect0 = Rect {
x: x + 12.,
y: y + (self.font_size as f32) + 8.,
w,
h,
};
let rect1 = Rect {
x: x + 12.,
y: y + h - (self.font_size as f32) + 4.,
w,
h,
};
let rect2 = Rect {
x: w.mul_add(0.5, x) + 8.,
y: y + h - (self.font_size as f32) + 4.,
w,
h,
};
vec![
(render, rect1, 1.0),
(render, rect0, 0.8),
(render, rect2, 0.8),
]
}
};
for (idx, (render, rect, alpha)) in renders.into_iter().enumerate() {
if idx >= label.len() {
break;
}
self.canvas.controls.push(Control {
rect,
text: Arc::from(label[idx].as_str()),
fg_color: self.fg_color * alpha,
size: self.font_size,
on_render_fg: Some(render),
..Control::new()
});
}
&mut self.canvas.controls[idx]
}
}

View File

@@ -1,387 +0,0 @@
use glam::Vec4;
use std::sync::Arc;
use vulkano::image::view::ImageView;
use crate::{
backend::input::PointerMode, graphics::WlxCommandBuffer, gui::GuiColor, state::AppState,
};
use super::{CanvasData, Rect};
pub type ControlRenderer<D, S> =
fn(&Control<D, S>, &CanvasData<D>, &mut AppState, &mut WlxCommandBuffer) -> anyhow::Result<()>;
pub type ControlRendererHl<D, S> = fn(
&Control<D, S>,
&CanvasData<D>,
&mut AppState,
&mut WlxCommandBuffer,
Vec4,
) -> anyhow::Result<()>;
#[allow(clippy::type_complexity)]
pub struct Control<D, S> {
pub state: Option<S>,
pub rect: Rect,
pub corner_radius: f32,
pub fg_color: GuiColor,
pub bg_color: GuiColor,
pub text: Arc<str>,
pub size: isize,
pub sprite: Option<Arc<ImageView>>,
pub sprite_st: Vec4,
pub(super) bg_dirty: bool,
pub(super) fg_dirty: bool,
pub on_update: Option<fn(&mut Self, &mut D, &mut AppState)>,
pub on_press: Option<fn(&mut Self, &mut D, &mut AppState, PointerMode)>,
pub on_release: Option<fn(&mut Self, &mut D, &mut AppState)>,
pub on_scroll: Option<fn(&mut Self, &mut D, &mut AppState, f32, f32)>,
pub test_highlight: Option<fn(&Self, &mut D, &mut AppState) -> Option<Vec4>>,
pub(super) on_render_bg: Option<ControlRenderer<D, S>>,
pub(super) on_render_hl: Option<ControlRendererHl<D, S>>,
pub(super) on_render_fg: Option<ControlRenderer<D, S>>,
}
impl<D, S> Control<D, S> {
pub(super) fn new() -> Self {
Self {
rect: Rect {
x: 0.,
y: 0.,
w: 0.,
h: 0.,
},
corner_radius: 0.,
fg_color: Vec4::ONE,
bg_color: Vec4::ZERO,
text: Arc::from(""),
sprite: None,
sprite_st: Vec4::new(1., 1., 0., 0.),
bg_dirty: true,
fg_dirty: true,
size: 24,
state: None,
on_update: None,
on_render_bg: None,
on_render_hl: None,
on_render_fg: None,
test_highlight: None,
on_press: None,
on_release: None,
on_scroll: None,
}
}
pub fn set_text(&mut self, text: &str) {
if *self.text == *text {
return;
}
self.text = text.into();
self.fg_dirty = true;
}
pub fn set_sprite(&mut self, sprite: Arc<ImageView>) {
self.sprite.replace(sprite);
self.bg_dirty = true;
}
pub fn set_sprite_st(&mut self, sprite_st: Vec4) {
if self.sprite_st == sprite_st {
return;
}
self.sprite_st = sprite_st;
self.bg_dirty = true;
}
pub fn set_fg_color(&mut self, color: GuiColor) {
if self.fg_color == color {
return;
}
self.fg_color = color;
self.fg_dirty = true;
}
pub fn render_rounded_rect(
&self,
canvas: &CanvasData<D>,
_: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let pass = {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let clamped_radius = self
.corner_radius
.min(self.rect.w / 2.0)
.min(self.rect.h / 2.0);
let skew_radius = [clamped_radius / self.rect.w, clamped_radius / self.rect.h];
let set0 = canvas.pipeline_bg_color.uniform_buffer(
0,
vec![
self.bg_color.x,
self.bg_color.y,
self.bg_color.z,
self.bg_color.w,
skew_radius[0],
skew_radius[1],
],
)?;
canvas.pipeline_bg_color.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0],
)?
};
cmd_buffer.run_ref(&pass)
}
pub(super) fn render_highlight(
&self,
canvas: &CanvasData<D>,
_: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
color: GuiColor,
) -> anyhow::Result<()> {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let clamped_radius = self
.corner_radius
.min(self.rect.w / 2.0)
.min(self.rect.h / 2.0);
let skew_radius = [clamped_radius / self.rect.w, clamped_radius / self.rect.h];
let set0 = canvas.pipeline_hl_color.uniform_buffer(
0,
vec![
color.x,
color.y,
color.z,
color.w,
skew_radius[0],
skew_radius[1],
],
)?;
let pass = canvas.pipeline_hl_color.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0],
)?;
cmd_buffer.run_ref(&pass)
}
pub(super) fn render_text(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let mut cur_y = self.rect.y;
for line in self.text.lines() {
let mut cur_x = self.rect.x;
for glyph in app
.fc
.get_glyphs(line, self.size, canvas.graphics.clone())?
{
if let Some(tex) = glyph.tex.clone() {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
cur_x + glyph.left,
cur_y - glyph.top,
glyph.width,
glyph.height,
)?;
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
0,
ImageView::new_default(tex)?,
app.graphics.texture_filtering,
)?;
let set1 = canvas
.pipeline_fg_glyph
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
let pass = canvas.pipeline_fg_glyph.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
}
cur_x += glyph.advance;
}
cur_y += (self.size as f32) * 1.5;
}
Ok(())
}
pub(super) fn render_text_centered(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let (w, h) = app
.fc
.get_text_size(&self.text, self.size, canvas.graphics.clone())?;
let mut cur_y =
(self.size as f32).mul_add(-0.25, h.mul_add(-0.5, self.rect.y + (self.rect.h)));
for line in self.text.lines() {
let mut cur_x = w.mul_add(-0.5, self.rect.w.mul_add(0.5, self.rect.x));
for glyph in app
.fc
.get_glyphs(line, self.size, canvas.graphics.clone())?
{
if let Some(tex) = glyph.tex.clone() {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
cur_x + glyph.left,
cur_y - glyph.top,
glyph.width,
glyph.height,
)?;
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
0,
ImageView::new_default(tex)?,
app.graphics.texture_filtering,
)?;
let set1 = canvas
.pipeline_fg_glyph
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
let pass = canvas.pipeline_fg_glyph.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
}
cur_x += glyph.advance;
}
cur_y += (self.size as f32) * 1.5;
}
Ok(())
}
pub(super) fn render_sprite_bg(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let Some(view) = self.sprite.as_ref() else {
return Ok(());
};
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let set0 = canvas.pipeline_bg_sprite.uniform_sampler(
0,
view.clone(),
app.graphics.texture_filtering,
)?;
let uniform = vec![
self.sprite_st.x,
self.sprite_st.y,
self.sprite_st.z,
self.sprite_st.w,
self.fg_color.x,
self.fg_color.y,
self.fg_color.z,
self.fg_color.w,
];
let set1 = canvas.pipeline_bg_sprite.uniform_buffer(1, uniform)?;
let pass = canvas.pipeline_bg_sprite.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
Ok(())
}
#[allow(dead_code)]
pub(super) fn render_sprite_hl(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
color: GuiColor,
) -> anyhow::Result<()> {
let Some(view) = self.sprite.as_ref() else {
return Ok(());
};
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let set0 = canvas.pipeline_hl_sprite.uniform_sampler(
0,
view.clone(),
app.graphics.texture_filtering,
)?;
let uniform = vec![
self.sprite_st.x,
self.sprite_st.y,
self.sprite_st.z,
self.sprite_st.w,
color.x,
color.y,
color.z,
color.w,
];
let set1 = canvas.pipeline_hl_sprite.uniform_buffer(1, uniform)?;
let pass = canvas.pipeline_hl_sprite.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
Ok(())
}
}

View File

@@ -1,371 +0,0 @@
pub mod builder;
pub mod control;
use std::sync::Arc;
use glam::{Vec2, Vec4};
use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::view::ImageView};
use crate::{
backend::{
input::{Haptics, InteractionHandler, PointerHit},
overlay::{FrameMeta, OverlayBackend, OverlayRenderer, ShouldRender},
},
graphics::{CommandBuffers, WlxGraphics, WlxPipeline, BLEND_ALPHA},
state::AppState,
};
const RES_DIVIDER: usize = 4;
pub struct Rect {
x: f32,
y: f32,
w: f32,
h: f32,
}
pub struct CanvasData<D> {
pub data: D,
pub width: usize,
pub height: usize,
graphics: Arc<WlxGraphics>,
pipeline_bg_color: Arc<WlxPipeline>,
pipeline_bg_sprite: Arc<WlxPipeline>,
pipeline_fg_glyph: Arc<WlxPipeline>,
pipeline_hl_color: Arc<WlxPipeline>,
pipeline_hl_sprite: Arc<WlxPipeline>,
}
pub struct Canvas<D, S> {
controls: Vec<control::Control<D, S>>,
data: CanvasData<D>,
hover_controls: [Option<usize>; 2],
pressed_controls: [Option<usize>; 2],
interact_map: Vec<Option<u16>>,
interact_stride: usize,
interact_rows: usize,
pipeline_final: Arc<WlxPipeline>,
view_fore: Arc<ImageView>,
view_back: Arc<ImageView>,
format: Format,
back_dirty: bool,
high_dirty: bool,
fore_dirty: bool,
}
impl<D, S> Canvas<D, S> {
fn new(
width: usize,
height: usize,
graphics: Arc<WlxGraphics>,
format: Format,
data: D,
) -> anyhow::Result<Self> {
let tex_fore = graphics.render_texture(width as _, height as _, format)?;
let tex_back = graphics.render_texture(width as _, height as _, format)?;
let view_fore = ImageView::new_default(tex_fore)?;
let view_back = ImageView::new_default(tex_back)?;
let Ok(shaders) = graphics.shared_shaders.read() else {
anyhow::bail!("Failed to lock shared shaders for reading");
};
let vert = shaders.get("vert_common").unwrap().clone(); // want panic
let pipeline_bg_color = graphics.create_pipeline(
vert.clone(),
shaders.get("frag_color").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let pipeline_fg_glyph = graphics.create_pipeline(
vert.clone(),
shaders.get("frag_glyph").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let pipeline_bg_sprite = graphics.create_pipeline(
vert.clone(),
shaders.get("frag_sprite2").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let pipeline_hl_color = graphics.create_pipeline(
vert.clone(),
shaders.get("frag_color").unwrap().clone(), // want panic
graphics.native_format,
Some(BLEND_ALPHA),
)?;
let pipeline_hl_sprite = graphics.create_pipeline(
vert.clone(),
shaders.get("frag_sprite2_hl").unwrap().clone(), // want panic
graphics.native_format,
Some(BLEND_ALPHA),
)?;
let pipeline_final = graphics.create_pipeline(
vert,
shaders.get("frag_srgb").unwrap().clone(), // want panic
graphics.native_format,
Some(BLEND_ALPHA),
)?;
let stride = width / RES_DIVIDER;
let rows = height / RES_DIVIDER;
Ok(Self {
data: CanvasData {
data,
width,
height,
graphics: graphics.clone(),
pipeline_bg_color,
pipeline_bg_sprite,
pipeline_fg_glyph,
pipeline_hl_color,
pipeline_hl_sprite,
},
controls: Vec::new(),
hover_controls: [None, None],
pressed_controls: [None, None],
interact_map: vec![None; stride * rows],
interact_stride: stride,
interact_rows: rows,
pipeline_final,
view_fore,
view_back,
format,
back_dirty: false,
high_dirty: false,
fore_dirty: false,
})
}
fn interactive_set_idx(&mut self, x: f32, y: f32, w: f32, h: f32, idx: usize) {
let (x, y, w, h) = (x as usize, y as usize, w as usize, h as usize);
let x_min = (x / RES_DIVIDER).max(0);
let y_min = (y / RES_DIVIDER).max(0);
let x_max = (x_min + (w / RES_DIVIDER)).min(self.interact_stride - 1);
let y_max = (y_min + (h / RES_DIVIDER)).min(self.interact_rows - 1);
for y in y_min..y_max {
for x in x_min..x_max {
self.interact_map[y * self.interact_stride + x] = Some(idx as u16);
}
}
}
fn interactive_get_idx(&self, uv: Vec2) -> Option<usize> {
let x = (uv.x * self.data.width as f32) as usize;
let y = (uv.y * self.data.height as f32) as usize;
let x = (x / RES_DIVIDER).max(0).min(self.interact_stride - 1);
let y = (y / RES_DIVIDER).max(0).min(self.interact_rows - 1);
self.interact_map[y * self.interact_stride + x].map(|x| x as usize)
}
pub const fn data_mut(&mut self) -> &mut D {
&mut self.data.data
}
}
impl<D, S> InteractionHandler for Canvas<D, S> {
fn on_left(&mut self, _app: &mut AppState, pointer: usize) {
self.high_dirty = true;
self.hover_controls[pointer] = None;
}
fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
// render on every frame if we are being hovered
self.high_dirty = true;
let old = self.hover_controls[hit.pointer];
if let Some(i) = self.interactive_get_idx(hit.uv) {
self.hover_controls[hit.pointer] = Some(i);
} else {
self.hover_controls[hit.pointer] = None;
}
if old == self.hover_controls[hit.pointer] {
None
} else {
Some(Haptics {
intensity: 0.1,
duration: 0.01,
frequency: 5.0,
})
}
}
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
let idx = if pressed {
self.interactive_get_idx(hit.uv)
} else {
self.pressed_controls[hit.pointer]
};
if let Some(idx) = idx {
let c = &mut self.controls[idx];
if pressed {
if let Some(ref mut f) = c.on_press {
self.pressed_controls[hit.pointer] = Some(idx);
f(c, &mut self.data.data, app, hit.mode);
}
} else if let Some(ref mut f) = c.on_release {
self.pressed_controls[hit.pointer] = None;
f(c, &mut self.data.data, app);
}
}
}
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta_y: f32, delta_x: f32) {
let idx = self.hover_controls[hit.pointer];
if let Some(idx) = idx {
let c = &mut self.controls[idx];
if let Some(ref mut f) = c.on_scroll {
f(c, &mut self.data.data, app, delta_y, delta_x);
}
}
}
}
impl<D, S> OverlayRenderer for Canvas<D, S> {
fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
for c in &mut self.controls {
if let Some(fun) = c.on_update {
fun(c, &mut self.data.data, app);
}
if c.fg_dirty {
self.fore_dirty = true;
c.fg_dirty = false;
}
if c.bg_dirty {
self.back_dirty = true;
c.bg_dirty = false;
}
}
if self.back_dirty || self.fore_dirty || self.high_dirty {
Ok(ShouldRender::Should)
} else {
Ok(ShouldRender::Can)
}
}
fn render(
&mut self,
app: &mut AppState,
tgt: Arc<ImageView>,
buf: &mut CommandBuffers,
alpha: f32,
) -> anyhow::Result<bool> {
self.high_dirty = false;
let mut cmd_buffer = self
.data
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
if self.back_dirty {
cmd_buffer.begin_rendering(self.view_back.clone())?;
for c in &mut self.controls {
if let Some(fun) = c.on_render_bg {
fun(c, &self.data, app, &mut cmd_buffer)?;
}
}
cmd_buffer.end_rendering()?;
self.back_dirty = false;
}
if self.fore_dirty {
cmd_buffer.begin_rendering(self.view_fore.clone())?;
for c in &mut self.controls {
if let Some(fun) = c.on_render_fg {
fun(c, &self.data, app, &mut cmd_buffer)?;
}
}
cmd_buffer.end_rendering()?;
self.fore_dirty = false;
}
let set0_fg = self.pipeline_final.uniform_sampler(
0,
self.view_fore.clone(),
app.graphics.texture_filtering,
)?;
let set0_bg = self.pipeline_final.uniform_sampler(
0,
self.view_back.clone(),
app.graphics.texture_filtering,
)?;
let set1 = self.pipeline_final.uniform_buffer(1, vec![alpha])?;
let pass_fore = self
.pipeline_final
.create_pass_for_target(tgt.clone(), vec![set0_fg, set1.clone()])?;
let pass_back = self
.pipeline_final
.create_pass_for_target(tgt.clone(), vec![set0_bg, set1])?;
cmd_buffer.begin_rendering(tgt)?;
cmd_buffer.run_ref(&pass_back)?;
for (i, c) in self.controls.iter_mut().enumerate() {
if let Some(render) = c.on_render_hl {
if let Some(test) = c.test_highlight {
if let Some(hl_color) = test(c, &mut self.data.data, app) {
render(c, &self.data, app, &mut cmd_buffer, hl_color)?;
}
}
if self.hover_controls.contains(&Some(i)) {
render(
c,
&self.data,
app,
&mut cmd_buffer,
Vec4::new(1., 1., 1., 0.3),
)?;
}
}
}
// mostly static text
cmd_buffer.run_ref(&pass_fore)?;
cmd_buffer.end_rendering()?;
buf.push(cmd_buffer.build()?);
Ok(true)
}
fn frame_meta(&mut self) -> Option<FrameMeta> {
Some(FrameMeta {
extent: [self.data.width as _, self.data.height as _, 1],
format: self.format,
..Default::default()
})
}
}
impl<D, S> OverlayBackend for Canvas<D, S> {
fn set_renderer(&mut self, _renderer: Box<dyn OverlayRenderer>) {}
fn set_interaction(&mut self, _interaction: Box<dyn InteractionHandler>) {}
}

View File

@@ -1,268 +0,0 @@
use std::{rc::Rc, str::FromStr, sync::Arc};
use fontconfig::{FontConfig, OwnedPattern};
use freetype::{bitmap::PixelMode, face::LoadFlag, Face, Library};
use idmap::IdMap;
use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::Image};
use crate::graphics::{WlxGraphics, WlxUploadsBuffer};
pub struct FontCache {
primary_font: Arc<str>,
fc: FontConfig,
ft: Library,
collections: IdMap<isize, FontCollection>,
}
struct FontCollection {
fonts: Vec<Font>,
cp_map: IdMap<usize, usize>,
zero_glyph: Rc<Glyph>,
}
struct Font {
face: Face,
glyphs: IdMap<usize, Rc<Glyph>>,
}
pub struct Glyph {
pub tex: Option<Arc<Image>>,
pub top: f32,
pub left: f32,
pub width: f32,
pub height: f32,
pub advance: f32,
}
impl FontCache {
pub fn new(primary_font: Arc<str>) -> anyhow::Result<Self> {
let ft = Library::init()?;
let fc = FontConfig::default();
Ok(Self {
primary_font,
fc,
ft,
collections: IdMap::new(),
})
}
pub fn get_text_size(
&mut self,
text: &str,
size: isize,
graphics: Arc<WlxGraphics>,
) -> anyhow::Result<(f32, f32)> {
let sizef = size as f32;
let height = ((text.lines().count() as f32) - 1f32).mul_add(sizef * 1.5, sizef);
let mut cmd_buffer = None;
let mut max_w = sizef * 0.33;
for line in text.lines() {
let w: f32 = line
.chars()
.filter_map(|c| {
self.get_glyph_for_cp(c as usize, size, graphics.clone(), &mut cmd_buffer)
.map(|glyph| glyph.advance)
.ok()
})
.sum();
if w > max_w {
max_w = w;
}
}
if let Some(cmd_buffer) = cmd_buffer {
cmd_buffer.build_and_execute_now()?;
}
Ok((max_w, height))
}
pub fn get_glyphs(
&mut self,
text: &str,
size: isize,
graphics: Arc<WlxGraphics>,
) -> anyhow::Result<Vec<Rc<Glyph>>> {
let mut glyphs = Vec::new();
let mut cmd_buffer = None;
for line in text.lines() {
for c in line.chars() {
glyphs.push(self.get_glyph_for_cp(
c as usize,
size,
graphics.clone(),
&mut cmd_buffer,
)?);
}
}
if let Some(cmd_buffer) = cmd_buffer {
cmd_buffer.build_and_execute_now()?;
}
Ok(glyphs)
}
fn get_font_for_cp(&mut self, cp: usize, size: isize) -> usize {
if !self.collections.contains_key(size) {
self.collections.insert(
size,
FontCollection {
fonts: Vec::new(),
cp_map: IdMap::new(),
zero_glyph: Rc::new(Glyph {
tex: None,
top: 0.,
left: 0.,
width: 0.,
height: 0.,
advance: size as f32 / 3.,
}),
},
);
}
let coll = self.collections.get_mut(size).unwrap(); // safe because of the insert above
if let Some(font) = coll.cp_map.get(cp) {
return *font;
}
let primary_font = self.primary_font.clone();
let pattern_str = format!("{primary_font}:size={size}:charset={cp:04x}");
let mut pattern = OwnedPattern::from_str(&pattern_str).unwrap(); // safe because PRIMARY_FONT is const
self.fc
.substitute(&mut pattern, fontconfig::MatchKind::Pattern);
pattern.default_substitute();
let pattern = pattern.font_match(&mut self.fc);
if let Some(path) = pattern.filename() {
let name = pattern.name().unwrap_or(path);
log::debug!("Loading font: {name} {size}pt");
let font_idx = pattern.face_index().unwrap_or(0);
let face = match self.ft.new_face(path, font_idx as _) {
Ok(face) => face,
Err(e) => {
log::warn!("Failed to load font at {path}: {e:?}");
coll.cp_map.insert(cp, 0);
return 0;
}
};
match face.set_char_size(size << 6, size << 6, 96, 96) {
Ok(()) => {}
Err(e) => {
log::warn!("Failed to set font size: {e:?}");
coll.cp_map.insert(cp, 0);
return 0;
}
}
let idx = coll.fonts.len();
for (cp, _) in face.chars() {
if coll.cp_map.contains_key(cp) {
continue;
}
coll.cp_map.insert(cp, idx);
}
if !coll.cp_map.contains_key(cp) {
log::warn!("Got font '{name}' for CP 0x{cp:x}, but CP is not present in font!",);
coll.cp_map.insert(cp, 0);
}
let zero_glyph = Rc::new(Glyph {
tex: None,
top: 0.,
left: 0.,
width: 0.,
height: 0.,
advance: size as f32 / 3.,
});
let mut glyphs = IdMap::new();
glyphs.insert(0, zero_glyph);
let font = Font { face, glyphs };
coll.fonts.push(font);
return idx;
}
coll.cp_map.insert(cp, 0);
0
}
fn get_glyph_for_cp(
&mut self,
cp: usize,
size: isize,
graphics: Arc<WlxGraphics>,
cmd_buffer: &mut Option<WlxUploadsBuffer>,
) -> anyhow::Result<Rc<Glyph>> {
let key = self.get_font_for_cp(cp, size);
let Some(font) = &mut self.collections[size].fonts.get_mut(key) else {
log::warn!("No font found for codepoint: 0x{cp:x}");
return Ok(self.collections[size].zero_glyph.clone());
};
if let Some(glyph) = font.glyphs.get(cp) {
return Ok(glyph.clone());
}
if font.face.load_char(cp, LoadFlag::DEFAULT).is_err() {
return Ok(self.collections[size].zero_glyph.clone());
}
let glyph = font.face.glyph();
if glyph.render_glyph(freetype::RenderMode::Normal).is_err() {
return Ok(self.collections[size].zero_glyph.clone());
}
let bmp = glyph.bitmap();
let buf = bmp.buffer().to_vec();
if buf.is_empty() {
return Ok(self.collections[size].zero_glyph.clone());
}
let metrics = glyph.metrics();
let format = match bmp.pixel_mode() {
Ok(PixelMode::Gray) => Format::R8_UNORM,
Ok(PixelMode::Gray2) => Format::R16_SFLOAT,
Ok(PixelMode::Gray4) => Format::R32_SFLOAT,
_ => return Ok(self.collections[size].zero_glyph.clone()),
};
if cmd_buffer.is_none() {
*cmd_buffer = Some(graphics.create_uploads_command_buffer(
graphics.transfer_queue.clone(),
CommandBufferUsage::OneTimeSubmit,
)?);
}
let texture = cmd_buffer.as_mut().unwrap().texture2d_raw(
bmp.width() as _,
bmp.rows() as _,
format,
&buf,
)?;
let g = Glyph {
tex: Some(texture),
top: (metrics.horiBearingY >> 6i64) as _,
left: (metrics.horiBearingX >> 6i64) as _,
advance: (metrics.horiAdvance >> 6i64) as _,
width: bmp.width() as _,
height: bmp.rows() as _,
};
font.glyphs.insert(cp, Rc::new(g));
Ok(font.glyphs[cp].clone())
}
}

View File

@@ -1,61 +1,3 @@
use std::sync::LazyLock; mod asset;
pub mod panel;
use glam::Vec4; mod timestep;
pub mod canvas;
pub mod font;
pub mod modular;
pub type GuiColor = Vec4;
pub static FALLBACK_COLOR: LazyLock<GuiColor> = LazyLock::new(|| Vec4::new(1., 0., 1., 1.));
// Parses a color from a HTML hex string
pub fn color_parse(html_hex: &str) -> anyhow::Result<GuiColor> {
if html_hex.len() == 7 {
if let (Ok(r), Ok(g), Ok(b)) = (
u8::from_str_radix(&html_hex[1..3], 16),
u8::from_str_radix(&html_hex[3..5], 16),
u8::from_str_radix(&html_hex[5..7], 16),
) {
return Ok(Vec4::new(
f32::from(r) / 255.,
f32::from(g) / 255.,
f32::from(b) / 255.,
1.,
));
}
} else if html_hex.len() == 9 {
if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
u8::from_str_radix(&html_hex[1..3], 16),
u8::from_str_radix(&html_hex[3..5], 16),
u8::from_str_radix(&html_hex[5..7], 16),
u8::from_str_radix(&html_hex[7..9], 16),
) {
return Ok(Vec4::new(
f32::from(r) / 255.,
f32::from(g) / 255.,
f32::from(b) / 255.,
f32::from(a) / 255.,
));
}
}
anyhow::bail!(
"Invalid color string: '{}', must be 7 characters long (e.g. #FF00FF)",
&html_hex
)
}
pub enum KeyCapType {
/// Label is in center of keycap
Regular,
/// Label on the top
/// AltGr symbol on bottom
RegularAltGr,
/// Primary symbol on bottom
/// Shift symbol on top
Reversed,
/// Primary symbol on bottom-left
/// Shift symbol on top-left
/// AltGr symbol on bottom-right
ReversedAltGr,
}

View File

@@ -1,867 +0,0 @@
use std::{
f32::consts::PI,
ops::Add,
process::{self, Child},
sync::Arc,
time::{Duration, Instant},
};
use glam::{Quat, Vec4};
use serde::Deserialize;
use crate::{
backend::{
common::OverlaySelector,
input::PointerMode,
overlay::Positioning,
task::{ColorChannel, SystemTask, TaskType},
},
config::{save_layout, save_settings, AStrSetExt},
hid::VirtualKey,
overlays::{
toast::{error_toast, Toast, ToastTopic},
watch::WATCH_NAME,
},
state::AppState,
};
#[cfg(any(not(feature = "wayvr"), not(feature = "osc")))]
use crate::overlays::toast::error_toast_str;
#[cfg(feature = "osc")]
use rosc::OscType;
use super::{ExecArgs, ModularControl, ModularData};
#[derive(Deserialize, Clone)]
pub enum PressRelease {
Release,
Press,
}
#[derive(Deserialize, Clone, Copy)]
pub enum ViewAngleKind {
/// The cosine of the angle at which the watch becomes fully transparent
MinOpacity,
/// The cosine of the angle at which the watch becomes fully opaque
MaxOpacity,
}
#[derive(Deserialize, Clone, Copy)]
pub enum Axis {
X,
Y,
Z,
}
#[derive(Deserialize, Clone)]
pub enum HighlightTest {
AllowSliding,
AutoRealign,
NotificationSounds,
Notifications,
RorateLock,
}
#[derive(Deserialize, Clone)]
pub enum SystemAction {
ToggleAllowSliding,
ToggleAutoRealign,
ToggleNotificationSounds,
ToggleNotifications,
ToggleRotateLock,
PlayspaceResetOffset,
PlayspaceFixFloor,
RecalculateExtent,
PersistConfig,
PersistLayout,
}
#[derive(Deserialize, Clone)]
pub enum WatchAction {
/// Hide the watch until Show/Hide binding is used
Hide,
/// Switch the watch to the opposite controller
SwitchHands,
/// Change the fade behavior of the watch
ViewAngle {
kind: ViewAngleKind,
delta: f32,
},
Rotation {
axis: Axis,
delta: f32,
},
Position {
axis: Axis,
delta: f32,
},
}
#[derive(Deserialize, Clone)]
pub enum OverlayAction {
/// Reset the overlay to be in front of the HMD with its original scale
Reset,
/// Toggle the visibility of the overlay
ToggleVisible,
/// Toggle the ability to grab and recenter the overlay
ToggleImmovable,
/// Toggle the ability of the overlay to reacto to laser pointer
ToggleInteraction,
/// Change the opacity of the overlay
Opacity { delta: f32 },
}
#[derive(Deserialize, Clone)]
pub enum WindowAction {
/// Create a new mirror window, or show/hide an existing one
ShowMirror,
/// Create a new UI window, or show/hide an existing one
ShowUi,
/// Destroy a previously created window, if it exists
Destroy,
}
#[derive(Deserialize, Clone)]
pub enum WayVRDisplayClickAction {
ToggleVisibility,
Reset,
}
#[derive(Deserialize, Clone)]
#[allow(dead_code)] // in case if WayVR feature is disabled
pub enum WayVRAction {
AppClick {
catalog_name: Arc<str>,
app_name: Arc<str>,
},
DisplayClick {
display_name: Arc<str>,
action: WayVRDisplayClickAction,
},
ToggleDashboard,
}
#[derive(Deserialize, Clone)]
#[serde(tag = "type")]
pub enum ButtonAction {
Exec {
command: ExecArgs,
toast: Option<Arc<str>>,
},
VirtualKey {
keycode: VirtualKey,
action: PressRelease,
},
Watch {
action: WatchAction,
},
Overlay {
target: OverlaySelector,
action: OverlayAction,
},
// Ignored if "wayvr" feature is not enabled
WayVR {
action: WayVRAction,
},
Window {
target: Arc<str>,
action: WindowAction,
},
Toast {
message: Arc<str>,
body: Option<Arc<str>>,
seconds: Option<f32>,
},
ColorAdjust {
channel: ColorChannel,
delta: f32,
},
DragMultiplier {
delta: f32,
},
System {
action: SystemAction,
},
SendOscValue {
parameter: Arc<str>,
values: Option<Vec<OscValue>>,
},
}
#[derive(Deserialize, Clone)]
#[serde(tag = "type")]
#[cfg(feature = "osc")]
pub enum OscValue {
Int { value: i32 },
Float { value: f32 },
String { value: String },
Bool { value: bool },
}
#[derive(Deserialize, Clone)]
#[serde(tag = "type")]
#[cfg(not(feature = "osc"))]
pub enum OscValue {
None,
}
pub(super) struct PressData {
last_down: Instant,
last_mode: PointerMode,
child: Option<Child>,
}
impl Clone for PressData {
fn clone(&self) -> Self {
Self {
last_down: self.last_down,
last_mode: self.last_mode,
child: None,
}
}
}
impl Default for PressData {
fn default() -> Self {
Self {
last_down: Instant::now(),
last_mode: PointerMode::Left,
child: None,
}
}
}
#[derive(Deserialize, Default, Clone)]
pub struct ButtonData {
#[serde(skip)]
pub(super) press: PressData,
pub(super) click_down: Option<Vec<ButtonAction>>,
pub(super) click_up: Option<Vec<ButtonAction>>,
pub(super) long_click_up: Option<Vec<ButtonAction>>,
pub(super) right_down: Option<Vec<ButtonAction>>,
pub(super) right_up: Option<Vec<ButtonAction>>,
pub(super) long_right_up: Option<Vec<ButtonAction>>,
pub(super) middle_down: Option<Vec<ButtonAction>>,
pub(super) middle_up: Option<Vec<ButtonAction>>,
pub(super) long_middle_up: Option<Vec<ButtonAction>>,
pub(super) scroll_down: Option<Vec<ButtonAction>>,
pub(super) scroll_up: Option<Vec<ButtonAction>>,
pub(super) highlight: Option<HighlightTest>,
}
pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) {
button.state = Some(ModularData::Button(Box::new(data.clone())));
button.on_press = Some(modular_button_dn);
button.on_release = Some(modular_button_up);
button.on_scroll = Some(modular_button_scroll);
button.test_highlight = Some(modular_button_highlight);
}
fn modular_button_dn(
button: &mut ModularControl,
_: &mut (),
app: &mut AppState,
mode: PointerMode,
) {
// want panic
let ModularData::Button(data) = button.state.as_mut().unwrap() else {
panic!("modular_button_dn: button state is not Button");
};
data.press.last_down = Instant::now();
data.press.last_mode = mode;
let actions = match mode {
PointerMode::Left => data.click_down.as_ref(),
PointerMode::Right => data.right_down.as_ref(),
PointerMode::Middle => data.middle_down.as_ref(),
_ => None,
};
if let Some(actions) = actions {
for action in actions {
handle_action(action, &mut data.press, app);
}
}
}
fn modular_button_up(button: &mut ModularControl, _: &mut (), app: &mut AppState) {
// want panic
let ModularData::Button(data) = button.state.as_mut().unwrap() else {
panic!("modular_button_up: button state is not Button");
};
let now = Instant::now();
let duration = now - data.press.last_down;
let long_press = duration.as_secs_f32() > app.session.config.long_press_duration;
let actions = match data.press.last_mode {
PointerMode::Left => {
if long_press {
data.long_click_up.as_ref()
} else {
data.click_up.as_ref()
}
}
PointerMode::Right => {
if long_press {
data.long_right_up.as_ref()
} else {
data.right_up.as_ref()
}
}
PointerMode::Middle => {
if long_press {
data.long_middle_up.as_ref()
} else {
data.middle_up.as_ref()
}
}
_ => None,
};
if let Some(actions) = actions {
for action in actions {
handle_action(action, &mut data.press, app);
}
}
}
fn modular_button_scroll(
button: &mut ModularControl,
_: &mut (),
app: &mut AppState,
delta_y: f32,
_delta_x: f32,
) {
// want panic
let ModularData::Button(data) = button.state.as_mut().unwrap() else {
panic!("modular_button_scroll: button state is not Button");
};
let actions = if delta_y < 0.0 {
data.scroll_down.as_ref()
} else {
data.scroll_up.as_ref()
};
if let Some(actions) = actions {
for action in actions {
handle_action(action, &mut data.press, app);
}
}
}
fn modular_button_highlight(
button: &ModularControl,
_: &mut (),
app: &mut AppState,
) -> Option<Vec4> {
// want panic
let ModularData::Button(data) = button.state.as_ref().unwrap() else {
panic!("modular_button_highlight: button state is not Button");
};
if let Some(test) = &data.highlight {
let lit = match test {
HighlightTest::AllowSliding => app.session.config.allow_sliding,
HighlightTest::AutoRealign => app.session.config.realign_on_showhide,
HighlightTest::NotificationSounds => app.session.config.notifications_sound_enabled,
HighlightTest::Notifications => app.session.config.notifications_enabled,
HighlightTest::RorateLock => !app.session.config.space_rotate_unlocked,
};
if lit {
return Some(Vec4::new(1.0, 1.0, 1.0, 0.5));
}
}
None
}
fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppState) {
match action {
ButtonAction::Exec { command, toast } => run_exec(command, toast.clone(), press, app),
ButtonAction::Watch { action } => run_watch(action, app),
ButtonAction::Overlay { target, action } => run_overlay(target, action, app),
ButtonAction::Window { target, action } => run_window(target, action, app),
ButtonAction::WayVR { action } => {
#[cfg(feature = "wayvr")]
{
app.tasks.enqueue(TaskType::WayVR(action.clone()));
}
#[cfg(not(feature = "wayvr"))]
{
let _ = &action;
error_toast_str(app, "WayVR feature is not enabled");
}
}
ButtonAction::VirtualKey { keycode, action } => app
.hid_provider
.send_key(*keycode, matches!(*action, PressRelease::Press)),
ButtonAction::Toast {
message,
body,
seconds,
} => {
Toast::new(
ToastTopic::System,
message.clone(),
body.clone().unwrap_or_else(|| "".into()),
)
.with_timeout(seconds.unwrap_or(5.))
.submit(app);
}
ButtonAction::ColorAdjust { channel, delta } => {
let channel = *channel;
let delta = *delta;
app.tasks
.enqueue(TaskType::System(SystemTask::ColorGain(channel, delta)));
}
ButtonAction::System { action } => run_system(action, app),
ButtonAction::DragMultiplier { delta } => {
app.session.config.space_drag_multiplier += delta;
}
ButtonAction::SendOscValue { parameter, values } => {
#[cfg(feature = "osc")]
if let Some(ref mut sender) = app.osc_sender {
// convert OscValue to OscType
let mut converted: Vec<OscType> = Vec::new();
for value in values.as_ref().unwrap() {
let converted_value = match value {
OscValue::Bool { value } => OscType::Bool(*value),
OscValue::Int { value } => OscType::Int(*value),
OscValue::Float { value } => OscType::Float(*value),
OscValue::String { value } => OscType::String(value.to_string()),
};
converted.push(converted_value);
}
let _ = sender.send_single_param(parameter.to_string(), converted);
audio_thump(app); // play sound for feedback
}
#[cfg(not(feature = "osc"))]
{
let _ = &parameter;
let _ = &values;
error_toast_str(app, "OSC feature is not enabled");
}
}
}
}
const ENABLED_DISABLED: [&str; 2] = ["enabled", "disabled"];
#[allow(clippy::too_many_lines)]
fn run_system(action: &SystemAction, app: &mut AppState) {
match action {
SystemAction::ToggleAllowSliding => {
app.session.config.allow_sliding = !app.session.config.allow_sliding;
Toast::new(
ToastTopic::System,
format!(
"Sliding is {}.",
ENABLED_DISABLED[usize::from(app.session.config.allow_sliding)]
)
.into(),
"".into(),
)
.submit(app);
}
SystemAction::ToggleAutoRealign => {
app.session.config.realign_on_showhide = !app.session.config.realign_on_showhide;
Toast::new(
ToastTopic::System,
format!(
"Auto realign is {}.",
ENABLED_DISABLED[usize::from(app.session.config.realign_on_showhide)]
)
.into(),
"".into(),
)
.submit(app);
}
SystemAction::ToggleRotateLock => {
app.session.config.space_rotate_unlocked = !app.session.config.space_rotate_unlocked;
Toast::new(
ToastTopic::System,
format!(
"Space rotate axis lock now {}.",
ENABLED_DISABLED[usize::from(!app.session.config.space_rotate_unlocked)]
)
.into(),
"".into(),
)
.submit(app);
}
SystemAction::PlayspaceResetOffset => {
app.tasks
.enqueue(TaskType::System(SystemTask::ResetPlayspace));
}
SystemAction::PlayspaceFixFloor => {
let now = Instant::now();
let sec = Duration::from_secs(1);
for i in 0..5 {
let at = now.add(i * sec);
let display = 5 - i;
Toast::new(
ToastTopic::System,
format!("Fixing floor in {display}").into(),
"Place either controller on the floor.".into(),
)
.with_timeout(1.0)
.submit_at(app, at);
}
app.tasks
.enqueue_at(TaskType::System(SystemTask::FixFloor), now.add(5 * sec));
}
SystemAction::RecalculateExtent => {
todo!()
}
SystemAction::ToggleNotifications => {
app.session.config.notifications_enabled = !app.session.config.notifications_enabled;
Toast::new(
ToastTopic::System,
format!(
"Notifications are {}.",
ENABLED_DISABLED[usize::from(app.session.config.notifications_enabled)]
)
.into(),
"".into(),
)
.submit(app);
}
SystemAction::ToggleNotificationSounds => {
app.session.config.notifications_sound_enabled =
!app.session.config.notifications_sound_enabled;
Toast::new(
ToastTopic::System,
format!(
"Notification sounds are {}.",
ENABLED_DISABLED[usize::from(app.session.config.notifications_sound_enabled)]
)
.into(),
"".into(),
)
.submit(app);
}
SystemAction::PersistConfig => {
if let Err(e) = save_settings(&app.session.config) {
error_toast(app, "Failed to save config", e);
}
}
SystemAction::PersistLayout => {
if let Err(e) = save_layout(&app.session.config) {
error_toast(app, "Failed to save layout", e);
}
}
}
}
fn run_exec(args: &ExecArgs, toast: Option<Arc<str>>, press: &mut PressData, app: &mut AppState) {
if let Some(proc) = press.child.as_mut() {
match proc.try_wait() {
Ok(Some(code)) => {
if !code.success() {
error_toast(app, "Child process exited with code", code);
}
press.child = None;
}
Ok(None) => {
log::warn!("Unable to launch child process: previous child not exited yet");
return;
}
Err(e) => {
press.child = None;
error_toast(app, "Error checking child process", e);
}
}
}
let args = args
.iter()
.map(std::convert::AsRef::as_ref)
.collect::<Vec<&str>>();
match process::Command::new(args[0]).args(&args[1..]).spawn() {
Ok(proc) => {
press.child = Some(proc);
if let Some(toast) = toast.as_ref() {
Toast::new(ToastTopic::System, toast.clone(), "".into()).submit(app);
}
}
Err(e) => {
error_toast(app, &format!("Failed to spawn process {args:?}"), e);
}
}
}
#[allow(clippy::too_many_lines)]
fn run_watch(data: &WatchAction, app: &mut AppState) {
match data {
WatchAction::Hide => {
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(WATCH_NAME.into()),
Box::new(|app, o| {
if o.saved_transform.is_none() {
o.want_visible = false;
o.saved_transform = Some(o.transform);
Toast::new(
ToastTopic::System,
"Watch hidden".into(),
"Use show/hide binding to restore.".into(),
)
.with_timeout(3.)
.submit(app);
} else {
o.want_visible = true;
o.saved_transform = None;
Toast::new(ToastTopic::System, "Watch restored".into(), "".into())
.submit(app);
}
}),
));
audio_thump(app);
}
WatchAction::SwitchHands => {
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(WATCH_NAME.into()),
Box::new(|app, o| {
if matches!(o.positioning, Positioning::FollowHand { hand: 0, .. }) {
o.positioning = Positioning::FollowHand { hand: 1, lerp: 1.0 };
o.spawn_rotation = app.session.config.watch_rot
* Quat::from_rotation_x(PI)
* Quat::from_rotation_z(PI);
o.spawn_point = app.session.config.watch_pos;
o.spawn_point.x *= -1.;
} else {
o.positioning = Positioning::FollowHand { hand: 0, lerp: 1.0 };
o.spawn_rotation = app.session.config.watch_rot;
o.spawn_point = app.session.config.watch_pos;
}
o.dirty = true;
Toast::new(
ToastTopic::System,
"Watch switched".into(),
"Check your other hand".into(),
)
.with_timeout(3.)
.submit(app);
}),
));
audio_thump(app);
}
WatchAction::ViewAngle { kind, delta } => match kind {
ViewAngleKind::MinOpacity => {
let diff = (app.session.config.watch_view_angle_max
- app.session.config.watch_view_angle_min)
+ delta;
app.session.config.watch_view_angle_min = (app.session.config.watch_view_angle_max
- diff)
.clamp(0.0, app.session.config.watch_view_angle_max - 0.05);
}
ViewAngleKind::MaxOpacity => {
let diff = app.session.config.watch_view_angle_max
- app.session.config.watch_view_angle_min;
app.session.config.watch_view_angle_max =
(app.session.config.watch_view_angle_max + delta).clamp(0.05, 1.0);
app.session.config.watch_view_angle_min = (app.session.config.watch_view_angle_max
- diff)
.clamp(0.0, app.session.config.watch_view_angle_max - 0.05);
}
},
WatchAction::Rotation { axis, delta } => {
let rot = match axis {
Axis::X => Quat::from_rotation_x(delta.to_radians()),
Axis::Y => Quat::from_rotation_y(delta.to_radians()),
Axis::Z => Quat::from_rotation_z(delta.to_radians()),
};
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(WATCH_NAME.into()),
Box::new(move |app, o| {
o.spawn_rotation *= rot;
app.session.config.watch_rot = o.spawn_rotation;
o.dirty = true;
}),
));
}
WatchAction::Position { axis, delta } => {
let delta = *delta;
let axis = match axis {
Axis::X => 0,
Axis::Y => 1,
Axis::Z => 2,
};
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(WATCH_NAME.into()),
Box::new(move |app, o| {
o.spawn_point[axis] += delta;
app.session.config.watch_pos = o.spawn_point;
o.dirty = true;
}),
));
}
}
}
#[allow(clippy::too_many_lines)]
fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppState) {
match action {
OverlayAction::Reset => {
app.tasks.enqueue(TaskType::Overlay(
overlay.clone(),
Box::new(|app, o| {
o.reset(app, true);
Toast::new(
ToastTopic::System,
format!("{} has been reset!", o.name).into(),
"".into(),
)
.submit(app);
}),
));
}
OverlayAction::ToggleVisible => {
app.tasks.enqueue(TaskType::Overlay(
overlay.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) => {
error_toast(app, "Failed to save state", e);
}
}
}
}),
));
}
OverlayAction::ToggleImmovable => {
app.tasks.enqueue(TaskType::Overlay(
overlay.clone(),
Box::new(|app, o| {
o.recenter = !o.recenter;
o.grabbable = o.recenter;
o.show_hide = o.recenter;
if o.recenter {
Toast::new(
ToastTopic::System,
format!("{} is now unlocked!", o.name).into(),
"".into(),
)
.submit(app);
} else {
Toast::new(
ToastTopic::System,
format!("{} is now locked in place!", o.name).into(),
"".into(),
)
.submit(app);
}
}),
));
audio_thump(app);
}
OverlayAction::ToggleInteraction => {
app.tasks.enqueue(TaskType::Overlay(
overlay.clone(),
Box::new(|app, o| {
o.interactable = !o.interactable;
if o.interactable {
Toast::new(
ToastTopic::System,
format!("{} is now interactable!", o.name).into(),
"".into(),
)
.submit(app);
} else {
Toast::new(
ToastTopic::System,
format!("{} is now non-interactable!", o.name).into(),
"".into(),
)
.submit(app);
}
}),
));
audio_thump(app);
}
OverlayAction::Opacity { delta } => {
let delta = *delta;
app.tasks.enqueue(TaskType::Overlay(
overlay.clone(),
Box::new(move |_, o| {
o.alpha = (o.alpha + delta).clamp(0.1, 1.0);
o.dirty = true;
log::debug!("{}: alpha {}", o.name, o.alpha);
}),
));
}
}
}
fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
use crate::overlays::custom;
match action {
WindowAction::ShowMirror => {
#[cfg(feature = "wayland")]
app.tasks.enqueue(TaskType::CreateOverlay(
OverlaySelector::Name(window.clone()),
Box::new({
let name = window.clone();
move |app| {
Toast::new(
ToastTopic::System,
"Check your desktop for popup.".into(),
"".into(),
)
.with_sound(true)
.submit(app);
Some(crate::overlays::mirror::new_mirror(
name,
false,
&app.session,
))
}
}),
));
#[cfg(not(feature = "wayland"))]
log::warn!("Mirror not available without Wayland feature.");
}
WindowAction::ShowUi => {
app.tasks.enqueue(TaskType::CreateOverlay(
OverlaySelector::Name(window.clone()),
Box::new({
let name = window.clone();
move |app| custom::create_custom(app, name)
}),
));
}
WindowAction::Destroy => {
app.tasks
.enqueue(TaskType::DropOverlay(OverlaySelector::Name(window.clone())));
}
}
}
const THUMP_AUDIO_WAV: &[u8] = include_bytes!("../../res/380885.wav");
fn audio_thump(app: &mut AppState) {
app.audio.play(THUMP_AUDIO_WAV);
}

View File

@@ -1,305 +0,0 @@
use chrono::Local;
use chrono_tz::Tz;
use glam::Vec4;
use smallvec::SmallVec;
use std::{
io::Read,
process::{self, Stdio},
sync::Arc,
time::Instant,
};
use crate::{
gui::modular::FALLBACK_COLOR,
overlays::toast::{error_toast, error_toast_str},
state::AppState,
};
use serde::Deserialize;
use super::{color_parse_or_default, ExecArgs, GuiColor, ModularControl, ModularData};
#[derive(Deserialize)]
#[serde(untagged)]
pub enum TimezoneDef {
Idx(usize),
Str(Arc<str>),
}
#[derive(Deserialize)]
#[serde(tag = "source")]
pub enum LabelContent {
Static {
text: Arc<str>,
},
Exec {
command: ExecArgs,
interval: f32,
},
Clock {
format: Arc<str>,
timezone: Option<TimezoneDef>,
},
Timezone {
timezone: usize,
},
Timer {
format: Arc<str>,
},
Battery {
device: usize,
low_threshold: f32,
low_color: Arc<str>,
charging_color: Arc<str>,
},
DragMultiplier,
Ipd,
}
pub enum LabelData {
Battery {
device: usize,
low_threshold: f32,
normal_color: GuiColor,
low_color: GuiColor,
charging_color: GuiColor,
},
Clock {
format: Arc<str>,
timezone: Option<Tz>,
},
Timer {
format: Arc<str>,
start: Instant,
},
Exec {
last_exec: Instant,
interval: f32,
command: Vec<Arc<str>>,
child: Option<process::Child>,
},
Ipd {
last_ipd: f32,
},
DragMultiplier,
}
pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent, app: &AppState) {
let state = match content {
LabelContent::Battery {
device,
low_threshold,
low_color,
charging_color,
} => Some(LabelData::Battery {
device: *device,
low_threshold: *low_threshold,
normal_color: label.fg_color,
low_color: color_parse_or_default(low_color),
charging_color: color_parse_or_default(charging_color),
}),
LabelContent::Clock { format, timezone } => {
let tz_str = match timezone {
Some(TimezoneDef::Idx(idx)) => app.session.config.timezones.get(*idx).map_or_else(
|| {
log::error!("Timezone index out of range '{idx}'");
label.set_fg_color(*FALLBACK_COLOR);
None
},
|tz| Some(tz.as_str()),
),
Some(TimezoneDef::Str(tz_str)) => Some(tz_str.as_ref()),
None => None,
};
Some(LabelData::Clock {
format: format.clone(),
timezone: tz_str.and_then(|tz| {
tz.parse()
.map_err(|_| {
log::error!("Failed to parse timezone '{}'", &tz);
label.set_fg_color(*FALLBACK_COLOR);
})
.ok()
}),
})
}
LabelContent::Timezone { timezone } => {
if let Some(tz) = app.session.config.timezones.get(*timezone) {
let pretty_tz = tz.split('/').next_back().map(|x| x.replace('_', " "));
if let Some(pretty_tz) = pretty_tz {
label.set_text(&pretty_tz);
return;
}
log::error!("Timezone name not valid '{}'", &tz);
} else {
log::error!("Timezone index out of range '{}'", &timezone);
}
label.set_fg_color(*FALLBACK_COLOR);
label.set_text("Error");
None
}
LabelContent::Timer { format } => Some(LabelData::Timer {
format: format.clone(),
start: Instant::now(),
}),
LabelContent::Exec { command, interval } => Some(LabelData::Exec {
last_exec: Instant::now(),
interval: *interval,
command: command.clone(),
child: None,
}),
LabelContent::Static { text } => {
label.set_text(text);
None
}
LabelContent::Ipd => Some(LabelData::Ipd { last_ipd: -1. }),
LabelContent::DragMultiplier => Some(LabelData::DragMultiplier),
};
if let Some(state) = state {
label.state = Some(ModularData::Label(Box::new(state)));
label.on_update = Some(label_update);
}
}
#[allow(clippy::too_many_lines)]
pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut AppState) {
// want panic
let ModularData::Label(data) = control.state.as_mut().unwrap() else {
panic!("Label control has no state");
};
match data.as_mut() {
LabelData::Battery {
device,
low_threshold,
normal_color,
low_color,
charging_color,
} => {
let device = app.input_state.devices.get(*device);
let tags = ["", "H", "L", "R", "T"];
if let Some(device) = device {
let (text, color) = device.soc.map_or_else(
|| (String::new(), Vec4::ZERO),
|soc| {
let text = format!(
"{}{}",
tags[device.role as usize],
(soc * 100.).min(99.) as u32
);
let color = if device.charging {
*charging_color
} else if soc < *low_threshold {
*low_color
} else {
*normal_color
};
(text, color)
},
);
control.set_text(&text);
control.set_fg_color(color);
} else {
control.set_text("");
}
}
LabelData::Clock { format, timezone } => {
let format = format.clone();
if let Some(tz) = timezone {
let date_time = Local::now().with_timezone(tz);
control.set_text(&format!("{}", &date_time.format(&format)));
} else {
let date_time = Local::now();
control.set_text(&format!("{}", &date_time.format(&format)));
}
}
LabelData::Timer { format, start } => {
let mut format = format.clone().to_lowercase();
let duration = start.elapsed().as_secs();
format = format.replace("%s", &format!("{:02}", (duration % 60)));
format = format.replace("%m", &format!("{:02}", ((duration / 60) % 60)));
format = format.replace("%h", &format!("{:02}", ((duration / 60) / 60)));
control.set_text(&format);
}
LabelData::Exec {
last_exec,
interval,
command,
child,
} => {
if let Some(mut proc) = child.take() {
match proc.try_wait() {
Ok(Some(code)) => {
if code.success() {
if let Some(mut stdout) = proc.stdout.take() {
let mut buf = String::new();
if stdout.read_to_string(&mut buf).is_ok() {
control.set_text(&buf);
} else {
error_toast_str(
app,
"LabelData::Exec: Failed to read stdout for child process",
);
return;
}
return;
}
log::error!("No stdout for child process");
return;
}
error_toast(app, "LabelData::Exec: Child process exited with code", code);
}
Ok(None) => {
*child = Some(proc);
// not exited yet
return;
}
Err(e) => {
*child = None;
error_toast(app, "Error checking child process", e);
return;
}
}
}
if Instant::now()
.saturating_duration_since(*last_exec)
.as_secs_f32()
> *interval
{
*last_exec = Instant::now();
let args = command
.iter()
.map(std::convert::AsRef::as_ref)
.collect::<SmallVec<[&str; 8]>>();
match process::Command::new(args[0])
.args(&args[1..])
.stdout(Stdio::piped())
.spawn()
{
Ok(proc) => {
*child = Some(proc);
}
Err(e) => {
error_toast(app, &format!("Failed to spawn process {args:?}"), e);
}
}
}
}
LabelData::Ipd { last_ipd } => {
if (app.input_state.ipd - *last_ipd).abs() > 0.05 {
*last_ipd = app.input_state.ipd;
control.set_text(&format!("{:.1}", app.input_state.ipd));
}
}
LabelData::DragMultiplier => {
control.set_text(&format!("{:.1}", app.session.config.space_drag_multiplier));
}
}
}

View File

@@ -1,569 +0,0 @@
pub mod button;
pub mod label;
use std::{fs::File, sync::Arc};
#[cfg(feature = "wayvr")]
use button::{WayVRAction, WayVRDisplayClickAction};
use glam::Vec4;
use serde::Deserialize;
use vulkano::{command_buffer::CommandBufferUsage, image::view::ImageView};
use crate::{
backend::common::OverlaySelector, config::AStrMapExt, config_io,
graphics::dds::WlxCommandBufferDds, state::AppState,
};
use self::{
button::{modular_button_init, ButtonAction, ButtonData, OverlayAction},
label::{modular_label_init, LabelContent, LabelData},
};
use super::{
canvas::{builder::CanvasBuilder, control::Control, Canvas},
color_parse, GuiColor, FALLBACK_COLOR,
};
type ModularControl = Control<(), ModularData>;
type ExecArgs = Vec<Arc<str>>;
#[derive(Deserialize)]
pub struct ModularUiConfig {
pub width: f32,
pub size: [u32; 2],
pub spawn_pos: Option<[f32; 3]>,
pub elements: Vec<ModularElement>,
}
#[derive(Deserialize)]
pub struct OverlayListTemplate {
click_down: Option<OverlayAction>,
click_up: Option<OverlayAction>,
long_click_up: Option<OverlayAction>,
right_down: Option<OverlayAction>,
right_up: Option<OverlayAction>,
long_right_up: Option<OverlayAction>,
middle_down: Option<OverlayAction>,
middle_up: Option<OverlayAction>,
long_middle_up: Option<OverlayAction>,
scroll_down: Option<OverlayAction>,
scroll_up: Option<OverlayAction>,
}
#[allow(dead_code)]
#[derive(Deserialize)]
#[serde(tag = "type")]
pub enum ModularElement {
Panel {
rect: [f32; 4],
corner_radius: Option<f32>,
bg_color: Arc<str>,
},
Label {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
#[serde(flatten)]
data: LabelContent,
},
CenteredLabel {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
#[serde(flatten)]
data: LabelContent,
},
Sprite {
rect: [f32; 4],
sprite: Arc<str>,
sprite_st: Option<[f32; 4]>,
},
Button {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
text: Arc<str>,
#[serde(flatten)]
data: Box<ButtonData>,
},
/// Convenience type to save you from having to create a bunch of labels
BatteryList {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
fg_color_low: Arc<str>,
fg_color_charging: Arc<str>,
low_threshold: f32,
num_devices: usize,
layout: ListLayout,
},
OverlayList {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
layout: ListLayout,
#[serde(flatten)]
template: Box<OverlayListTemplate>,
},
// Ignored if "wayvr" feature is not enabled
WayVRLauncher {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
catalog_name: Arc<str>,
},
// Ignored if "wayvr" feature is not enabled
WayVRDisplayList {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
},
}
#[derive(Deserialize, Clone)]
pub enum ButtonFunc {
HideWatch,
SwitchWatchHand,
}
#[derive(Deserialize)]
pub enum ListLayout {
Horizontal,
Vertical,
}
pub enum ModularData {
Label(Box<LabelData>),
Button(Box<ButtonData>),
}
#[allow(clippy::too_many_lines, clippy::many_single_char_names)]
pub fn modular_canvas(
size: [u32; 2],
elements: &[ModularElement],
state: &mut AppState,
) -> anyhow::Result<Canvas<(), ModularData>> {
let mut canvas = CanvasBuilder::new(
size[0] as _,
size[1] as _,
state.graphics.clone(),
state.graphics.native_format,
(),
)?;
let empty_str: Arc<str> = Arc::from("");
for elem in elements {
match elem {
ModularElement::Panel {
rect: [x, y, w, h],
corner_radius,
bg_color,
} => {
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
canvas.panel(*x, *y, *w, *h, corner_radius.unwrap_or_default());
}
ModularElement::Label {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
data,
} => {
canvas.font_size = *font_size;
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
let label = canvas.label(
*x,
*y,
*w,
*h,
corner_radius.unwrap_or_default(),
empty_str.clone(),
);
modular_label_init(label, data, state);
}
ModularElement::CenteredLabel {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
data,
} => {
canvas.font_size = *font_size;
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
let label = canvas.label_centered(
*x,
*y,
*w,
*h,
corner_radius.unwrap_or_default(),
empty_str.clone(),
);
modular_label_init(label, data, state);
}
ModularElement::Sprite {
rect: [x, y, w, h],
sprite,
sprite_st,
} => match sprite_from_path(sprite.clone(), state) {
Ok(view) => {
let sprite = canvas.sprite(*x, *y, *w, *h);
sprite.fg_color = Vec4::ONE;
sprite.set_sprite(view);
let st = sprite_st
.map(|st| Vec4::from_slice(&st))
.unwrap_or_else(|| Vec4::new(1., 1., 0., 0.));
sprite.set_sprite_st(st);
}
Err(e) => {
log::warn!("Could not load custom UI sprite: {e:?}");
}
},
ModularElement::Button {
rect: [x, y, w, h],
corner_radius,
font_size,
bg_color,
fg_color,
text,
data,
} => {
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
canvas.font_size = *font_size;
let button = canvas.button(
*x,
*y,
*w,
*h,
corner_radius.unwrap_or_default(),
text.clone(),
);
modular_button_init(button, data);
}
ModularElement::BatteryList {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
fg_color_low,
fg_color_charging,
low_threshold,
num_devices,
layout,
} => {
let num_buttons = *num_devices as f32;
let mut button_x = *x;
let mut button_y = *y;
let low_threshold = low_threshold * 0.01;
let (button_w, button_h) = match layout {
ListLayout::Horizontal => (*w / num_buttons, *h),
ListLayout::Vertical => (*w, *h / num_buttons),
};
let fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
canvas.font_size = *font_size;
canvas.fg_color = fg_color;
for i in 0..*num_devices {
let label = canvas.label_centered(
button_x + 2.,
button_y + 2.,
button_w - 4.,
button_h - 4.,
corner_radius.unwrap_or_default(),
empty_str.clone(),
);
modular_label_init(
label,
&LabelContent::Battery {
device: i,
low_threshold,
low_color: fg_color_low.clone(),
charging_color: fg_color_charging.clone(),
},
state,
);
button_x += match layout {
ListLayout::Horizontal => button_w,
ListLayout::Vertical => 0.,
};
button_y += match layout {
ListLayout::Horizontal => 0.,
ListLayout::Vertical => button_h,
};
}
}
ModularElement::OverlayList {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
bg_color,
layout,
template,
} => {
let num_buttons = state.screens.len() as f32;
let mut button_x = *x;
let mut button_y = *y;
let (button_w, button_h) = match layout {
ListLayout::Horizontal => (*w / num_buttons, *h),
ListLayout::Vertical => (*w, *h / num_buttons),
};
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
canvas.font_size = *font_size;
for screen in &state.screens {
let button = canvas.button(
button_x + 2.,
button_y + 2.,
button_w - 4.,
button_h - 4.,
corner_radius.unwrap_or_default(),
screen.name.clone(),
);
// cursed
let data = ButtonData {
click_down: template.click_down.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
click_up: template.click_up.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
long_click_up: template.long_click_up.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
right_down: template.right_down.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
right_up: template.right_up.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
long_right_up: template.long_right_up.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
middle_down: template.middle_down.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
middle_up: template.middle_up.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
long_middle_up: template.long_middle_up.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
scroll_down: template.scroll_down.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
scroll_up: template.scroll_up.as_ref().map(|f| {
vec![ButtonAction::Overlay {
target: OverlaySelector::Id(screen.id),
action: f.clone(),
}]
}),
..Default::default()
};
modular_button_init(button, &data);
button_x += match layout {
ListLayout::Horizontal => button_w,
ListLayout::Vertical => 0.,
};
button_y += match layout {
ListLayout::Horizontal => 0.,
ListLayout::Vertical => button_h,
};
}
}
#[allow(unused_variables)] // needed in case if wayvr feature is not enabled
ModularElement::WayVRLauncher {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
bg_color,
catalog_name,
} => {
#[cfg(feature = "wayvr")]
{
if let Some(catalog) = state.session.wayvr_config.get_catalog(catalog_name) {
let mut button_x = *x;
let button_y = *y;
for app in &catalog.apps {
let button_w: f32 = *w / catalog.apps.len() as f32;
let button_h: f32 = *h;
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
canvas.font_size = *font_size;
let button = canvas.button(
button_x + 2.,
button_y + 2.,
button_w - 4.,
button_h - 4.,
corner_radius.unwrap_or_default(),
Arc::from(app.name.as_str()),
);
let data = ButtonData {
click_up: Some(vec![ButtonAction::WayVR {
action: WayVRAction::AppClick {
catalog_name: catalog_name.clone(),
app_name: Arc::from(app.name.as_str()),
},
}]),
..Default::default()
};
modular_button_init(button, &data);
button_x += button_w;
}
} else {
log::error!("WayVR catalog \"{catalog_name}\" not found");
}
}
#[cfg(not(feature = "wayvr"))]
{
log::error!("WayVR feature is not enabled, ignoring");
}
}
#[allow(unused_variables)]
ModularElement::WayVRDisplayList {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
bg_color,
} => {
#[cfg(feature = "wayvr")]
{
let mut button_x = *x;
let button_y = *y;
let displays = &state.session.wayvr_config.displays;
for (display_name, display) in displays {
let button_w: f32 = (*w / displays.len() as f32).min(80.0);
let button_h: f32 = *h;
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
canvas.font_size = *font_size;
let button = canvas.button(
button_x + 2.,
button_y + 2.,
button_w - 4.,
button_h - 4.,
corner_radius.unwrap_or_default(),
Arc::from(display_name.as_str()),
);
let data = ButtonData {
click_up: Some(vec![ButtonAction::WayVR {
action: WayVRAction::DisplayClick {
display_name: Arc::from(display_name.as_str()),
action: WayVRDisplayClickAction::ToggleVisibility,
},
}]),
long_click_up: Some(vec![ButtonAction::WayVR {
action: WayVRAction::DisplayClick {
display_name: Arc::from(display_name.as_str()),
action: WayVRDisplayClickAction::Reset,
},
}]),
..Default::default()
};
modular_button_init(button, &data);
button_x += button_w;
}
}
#[cfg(not(feature = "wayvr"))]
{
log::error!("WayVR feature is not enabled, ignoring")
}
}
}
}
Ok(canvas.build())
}
pub fn color_parse_or_default(color: &str) -> GuiColor {
color_parse(color).unwrap_or_else(|e| {
log::error!("Failed to parse color '{color}': {e}");
*FALLBACK_COLOR
})
}
fn sprite_from_path(path: Arc<str>, app: &mut AppState) -> anyhow::Result<Arc<ImageView>> {
if let Some(view) = app.sprites.arc_get(&path) {
return Ok(view.clone());
}
let real_path = config_io::get_config_root().join(&*path);
let Ok(f) = File::open(real_path) else {
anyhow::bail!("Could not open custom sprite at: {}", path);
};
let mut command_buffer = app.graphics.create_uploads_command_buffer(
app.graphics.transfer_queue.clone(),
CommandBufferUsage::OneTimeSubmit,
)?;
match command_buffer.texture2d_dds(f) {
Ok(image) => {
command_buffer.build_and_execute_now()?;
Ok(ImageView::new_default(image)?)
}
Err(e) => {
anyhow::bail!("Could not use custom sprite at: {}\n{:?}", path, e);
}
}
}

161
src/gui/panel.rs Normal file
View File

@@ -0,0 +1,161 @@
use std::sync::Arc;
use glam::vec2;
use vulkano::{command_buffer::CommandBufferUsage, image::view::ImageView};
use wgui::{
event::{Event as WguiEvent, MouseDownEvent, MouseMotionEvent, MouseUpEvent, MouseWheelEvent},
layout::Layout,
renderer_vk::context::Context as WguiContext,
};
use crate::{
backend::{
input::{Haptics, InteractionHandler, PointerHit},
overlay::{FrameMeta, OverlayBackend, OverlayRenderer, ShouldRender},
},
graphics::{CommandBuffers, ExtentExt},
state::AppState,
};
use super::{asset::GuiAsset, timestep::Timestep};
pub struct GuiPanel {
pub layout: Layout,
context: WguiContext,
timestep: Timestep,
pub width: u32,
pub height: u32,
}
impl GuiPanel {
pub fn new_from_template(
app: &AppState,
width: u32,
height: u32,
path: &str,
) -> anyhow::Result<Self> {
let mut me = Self::new_blank(app, width, height)?;
let parent = me.layout.root_widget;
let _res = wgui::parser::parse_from_assets(&mut me.layout, parent, path)?;
Ok(me)
}
pub fn new_blank(app: &AppState, width: u32, height: u32) -> anyhow::Result<Self> {
let layout = Layout::new(Box::new(GuiAsset {}))?;
let context = WguiContext::new(app.gfx.clone(), app.gfx.surface_format, 1.0)?;
let mut timestep = Timestep::new();
timestep.set_tps(60.0);
Ok(Self {
layout,
context,
timestep,
width,
height,
})
}
}
impl OverlayBackend for GuiPanel {
fn set_renderer(&mut self, _: Box<dyn OverlayRenderer>) {
log::debug!("Attempted to replace renderer on GuiPanel!");
}
fn set_interaction(&mut self, _: Box<dyn InteractionHandler>) {
log::debug!("Attempted to replace interaction layer on GuiPanel!");
}
}
impl InteractionHandler for GuiPanel {
fn on_scroll(&mut self, _app: &mut AppState, hit: &PointerHit, delta_y: f32, delta_x: f32) {
self.layout
.push_event(&WguiEvent::MouseWheel(MouseWheelEvent {
shift: vec2(delta_x, delta_y),
pos: hit.uv,
}))
.unwrap()
}
fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
self.layout
.push_event(&WguiEvent::MouseMotion(MouseMotionEvent { pos: hit.uv }))
.unwrap();
None
}
fn on_left(&mut self, _app: &mut AppState, _pointer: usize) {
//TODO: is this needed?
}
fn on_pointer(&mut self, _app: &mut AppState, hit: &PointerHit, pressed: bool) {
if pressed {
self.layout
.push_event(&WguiEvent::MouseDown(MouseDownEvent { pos: hit.uv }))
.unwrap();
} else {
self.layout
.push_event(&WguiEvent::MouseUp(MouseUpEvent { pos: hit.uv }))
.unwrap();
}
}
}
impl OverlayRenderer for GuiPanel {
fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
self.timestep.reset();
Ok(())
}
fn should_render(&mut self, _app: &mut AppState) -> anyhow::Result<ShouldRender> {
while self.timestep.on_tick() {
self.layout.tick()?;
}
Ok(if self.layout.check_toggle_needs_redraw() {
ShouldRender::Should
} else {
ShouldRender::Can
})
}
fn render(
&mut self,
app: &mut AppState,
tgt: Arc<ImageView>,
buf: &mut CommandBuffers,
_alpha: f32,
) -> anyhow::Result<bool> {
self.context.update_viewport(tgt.extent_u32arr(), 1.0)?;
self.layout.update(tgt.extent_vec2(), self.timestep.alpha);
let mut cmd_buf = app
.gfx
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)
.unwrap();
cmd_buf.begin_rendering(tgt)?;
let primitives = wgui::drawing::draw(&self.layout)?;
self.context.draw(&app.gfx, &mut cmd_buf, &primitives)?;
cmd_buf.end_rendering()?;
buf.push(cmd_buf.build()?);
Ok(true)
}
fn frame_meta(&mut self) -> Option<FrameMeta> {
Some(FrameMeta {
extent: [self.width, self.height, 1],
..Default::default()
})
}
}

70
src/gui/timestep.rs Normal file
View File

@@ -0,0 +1,70 @@
use std::{sync::LazyLock, time::Instant};
static TIME_START: LazyLock<Instant> = LazyLock::new(Instant::now);
pub fn get_micros() -> u64 {
TIME_START.elapsed().as_micros() as u64
}
#[derive(Default)]
pub struct Timestep {
current_time_us: u64,
accumulator: f32,
time_micros: u64,
ticks: u32,
speed: f32,
pub alpha: f32,
delta: f32,
loopnum: u8,
}
impl Timestep {
pub fn new() -> Timestep {
let mut timestep = Timestep {
speed: 1.0,
..Default::default()
};
timestep.reset();
timestep
}
fn calculate_alpha(&mut self) {
self.alpha = (self.accumulator / self.delta).clamp(0.0, 1.0);
}
pub fn set_tps(&mut self, tps: f32) {
self.delta = 1000.0 / tps;
}
pub fn reset(&mut self) {
self.current_time_us = get_micros();
self.accumulator = 0.0;
}
pub fn on_tick(&mut self) -> bool {
let newtime = get_micros();
let frametime = newtime - self.current_time_us;
self.time_micros += frametime;
self.current_time_us = newtime;
self.accumulator += frametime as f32 * self.speed / 1000.0;
self.calculate_alpha();
if self.accumulator >= self.delta {
self.accumulator -= self.delta;
self.loopnum += 1;
self.ticks += 1;
if self.loopnum > 5 {
// cannot keep up!
self.loopnum = 0;
self.accumulator = 0.0;
return false;
}
true
} else {
self.loopnum = 0;
false
}
}
}

View File

@@ -113,12 +113,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(()); return Ok(());
} }
#[cfg(feature = "uidev")]
if let Some(panel_name) = args.uidev.as_ref() {
crate::backend::uidev::uidev_run(panel_name.as_str())?;
return Ok(());
}
let running = Arc::new(AtomicBool::new(true)); let running = Arc::new(AtomicBool::new(true));
let _ = ctrlc::set_handler({ let _ = ctrlc::set_handler({
let running = running.clone(); let running = running.clone();

View File

@@ -1,18 +1,61 @@
use glam::Vec3A; use glam::Vec3A;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use wgui::parser::parse_color_hex;
use wgui::renderer_vk::text::{FontWeight, TextStyle};
use wgui::taffy;
use wgui::taffy::prelude::{length, percent};
use wgui::widget::rectangle::{Rectangle, RectangleParams};
use wgui::widget::text::{TextLabel, TextParams};
use wgui::widget::util::WLength;
use crate::backend::overlay::{OverlayData, OverlayState, Positioning, Z_ORDER_ANCHOR}; use crate::backend::overlay::{OverlayData, OverlayState, Positioning, Z_ORDER_ANCHOR};
use crate::config::{load_known_yaml, ConfigType}; use crate::gui::panel::GuiPanel;
use crate::gui::modular::{modular_canvas, ModularUiConfig};
use crate::state::AppState; use crate::state::AppState;
pub static ANCHOR_NAME: LazyLock<Arc<str>> = LazyLock::new(|| Arc::from("anchor")); pub static ANCHOR_NAME: LazyLock<Arc<str>> = LazyLock::new(|| Arc::from("anchor"));
pub fn create_anchor<O>(state: &mut AppState) -> anyhow::Result<OverlayData<O>> pub fn create_anchor<O>(app: &mut AppState) -> anyhow::Result<OverlayData<O>>
where where
O: Default, O: Default,
{ {
let config = load_known_yaml::<ModularUiConfig>(ConfigType::Anchor); let mut panel = GuiPanel::new_blank(app, 200, 200)?;
let (rect, _) = panel.layout.add_child(
panel.layout.root_widget,
Rectangle::create(RectangleParams {
color: wgui::drawing::Color::new(0., 0., 0., 0.),
border_color: parse_color_hex("#ffff00").unwrap(),
border: 2.0,
round: WLength::Percent(1.0),
..Default::default()
})
.unwrap(),
taffy::Style {
size: taffy::Size {
width: percent(1.0),
height: percent(1.0),
},
align_items: Some(taffy::AlignItems::Center),
justify_content: Some(taffy::JustifyContent::Center),
padding: length(4.0),
..Default::default()
},
)?;
let _ = panel.layout.add_child(
rect,
TextLabel::create(TextParams {
content: "Center".into(),
style: TextStyle {
weight: Some(FontWeight::Bold),
size: Some(36.0),
color: parse_color_hex("#ffff00"),
..Default::default()
},
})
.unwrap(),
taffy::style::Style::DEFAULT,
);
Ok(OverlayData { Ok(OverlayData {
state: OverlayState { state: OverlayState {
@@ -21,12 +64,12 @@ where
interactable: false, interactable: false,
grabbable: false, grabbable: false,
z_order: Z_ORDER_ANCHOR, z_order: Z_ORDER_ANCHOR,
spawn_scale: config.width, spawn_scale: 0.1,
spawn_point: Vec3A::NEG_Z * 0.5, spawn_point: Vec3A::NEG_Z * 0.5,
positioning: Positioning::Static, positioning: Positioning::Static,
..Default::default() ..Default::default()
}, },
backend: Box::new(modular_canvas(config.size, &config.elements, state)?), backend: Box::new(panel),
..Default::default() ..Default::default()
}) })
} }

View File

@@ -3,49 +3,34 @@ use std::sync::Arc;
use glam::Vec3A; use glam::Vec3A;
use crate::{ use crate::{
backend::overlay::{ui_transform, OverlayBackend, OverlayState}, backend::overlay::{OverlayBackend, OverlayState},
config::{load_custom_ui, load_known_yaml, ConfigType}, gui::panel::GuiPanel,
gui::modular::{modular_canvas, ModularUiConfig},
state::AppState, state::AppState,
}; };
const SETTINGS_NAME: &str = "settings"; const SETTINGS_NAME: &str = "settings";
pub fn create_custom( pub fn create_custom(
state: &mut AppState, app: &mut AppState,
name: Arc<str>, name: Arc<str>,
) -> Option<(OverlayState, Box<dyn OverlayBackend>)> { ) -> Option<(OverlayState, Box<dyn OverlayBackend>)> {
let config = if &*name == SETTINGS_NAME {
load_known_yaml::<ModularUiConfig>(ConfigType::Settings)
} else {
match load_custom_ui(&name) {
Ok(config) => config,
Err(e) => {
log::error!("Failed to load custom UI config for {name}: {e:?}");
return None; return None;
}
}
};
let canvas = match modular_canvas(config.size, &config.elements, state) { unreachable!();
Ok(canvas) => canvas,
Err(e) => { let panel = GuiPanel::new_blank(&app, 200, 200).ok()?;
log::error!("Failed to create canvas for {name}: {e:?}");
return None;
}
};
let state = OverlayState { let state = OverlayState {
name, name,
want_visible: true, want_visible: true,
interactable: true, interactable: true,
grabbable: true, grabbable: true,
spawn_scale: config.width, spawn_scale: 0.1, //TODO: this
spawn_point: Vec3A::from_array(config.spawn_pos.unwrap_or([0., 0., -0.5])), spawn_point: Vec3A::from_array([0., 0., -0.5]),
interaction_transform: ui_transform(config.size), //interaction_transform: ui_transform(config.size),
..Default::default() ..Default::default()
}; };
let backend = Box::new(canvas); let backend = Box::new(panel);
Some((state, backend)) Some((state, backend))
} }

View File

@@ -15,20 +15,26 @@ use crate::{
}, },
config::{self, ConfigType}, config::{self, ConfigType},
graphics::CommandBuffers, graphics::CommandBuffers,
gui::{ gui::panel::GuiPanel,
canvas::{builder::CanvasBuilder, control::Control, Canvas},
color_parse, KeyCapType,
},
hid::{ hid::{
get_key_type, KeyModifier, KeyType, VirtualKey, XkbKeymap, ALT, CTRL, KEYS_TO_MODS, META, get_key_type, KeyModifier, KeyType, VirtualKey, XkbKeymap, ALT, CTRL, KEYS_TO_MODS, META,
NUM_LOCK, SHIFT, SUPER, NUM_LOCK, SHIFT, SUPER,
}, },
state::{AppState, KeyboardFocus}, state::{AppState, KeyboardFocus},
}; };
use glam::{vec2, vec3a, Affine2, Vec4}; use glam::{vec2, vec3a, Affine2, Vec2Swizzles, Vec4};
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use vulkano::image::view::ImageView; use vulkano::image::view::ImageView;
use wgui::{
parser::parse_color_hex,
taffy::{self, prelude::length},
widget::{
div::Div,
rectangle::{Rectangle, RectangleParams},
util::WLength,
},
};
const PIXELS_PER_UNIT: f32 = 80.; const PIXELS_PER_UNIT: f32 = 80.;
const BUTTON_PADDING: f32 = 4.; const BUTTON_PADDING: f32 = 4.;
@@ -92,20 +98,28 @@ where
processes: vec![], processes: vec![],
}; };
let mut canvas = CanvasBuilder::new( let padding = 4f32;
size.x as _,
size.y as _, let mut panel = GuiPanel::new_blank(
app.graphics.clone(), app,
app.graphics.native_format, padding.mul_add(2.0, size.x) as u32,
data, padding.mul_add(2.0, size.y) as u32,
)?; )?;
canvas.bg_color = color_parse("#181926").unwrap(); //safe let (background, _) = panel.layout.add_child(
canvas.panel(0., 0., size.x, size.y, 12.); panel.layout.root_widget,
Rectangle::create(RectangleParams {
canvas.font_size = 18; color: wgui::drawing::Color::new(0., 0., 0., 0.6),
canvas.fg_color = color_parse("#cad3f5").unwrap(); //safe round: WLength::Units(4.0),
canvas.bg_color = color_parse("#1e2030").unwrap(); //safe ..Default::default()
})
.unwrap(),
taffy::Style {
flex_direction: taffy::FlexDirection::Column,
padding: length(padding),
..Default::default()
},
)?;
let has_altgr = keymap let has_altgr = keymap
.as_ref() .as_ref()
@@ -115,17 +129,22 @@ where
keymap = None; keymap = None;
} }
let unit_size = size.x / LAYOUT.row_size;
let h = 2.0f32.mul_add(-BUTTON_PADDING, unit_size);
for row in 0..LAYOUT.key_sizes.len() { for row in 0..LAYOUT.key_sizes.len() {
let y = unit_size.mul_add(row as f32, BUTTON_PADDING); let (div, _) = panel.layout.add_child(
let mut sum_size = 0f32; background,
Div::create().unwrap(),
taffy::Style {
flex_direction: taffy::FlexDirection::Row,
..Default::default()
},
)?;
for col in 0..LAYOUT.key_sizes[row].len() { for col in 0..LAYOUT.key_sizes[row].len() {
let my_size = LAYOUT.key_sizes[row][col]; let my_size = LAYOUT.key_sizes[row][col];
let x = unit_size.mul_add(sum_size, BUTTON_PADDING); let my_size = taffy::Size {
let w = unit_size.mul_add(my_size, -(2. * BUTTON_PADDING)); width: length(PIXELS_PER_UNIT * my_size),
height: length(PIXELS_PER_UNIT),
};
if let Some(key) = LAYOUT.main_layout[row][col].as_ref() { if let Some(key) = LAYOUT.main_layout[row][col].as_ref() {
let mut label = Vec::with_capacity(2); let mut label = Vec::with_capacity(2);
@@ -180,8 +199,7 @@ where
} else if let Some(exec_args) = LAYOUT.exec_commands.get(key) { } else if let Some(exec_args) = LAYOUT.exec_commands.get(key) {
if exec_args.is_empty() { if exec_args.is_empty() {
log::error!("Keyboard: EXEC args empty for {key}"); log::error!("Keyboard: EXEC args empty for {key}");
continue; } else {
}
let mut iter = exec_args.iter().cloned(); let mut iter = exec_args.iter().cloned();
if let Some(program) = iter.next() { if let Some(program) = iter.next() {
maybe_state = Some(KeyButtonData::Exec { maybe_state = Some(KeyButtonData::Exec {
@@ -191,6 +209,7 @@ where
release_args: iter.collect(), release_args: iter.collect(),
}); });
} }
}
} else { } else {
log::error!("Unknown key: {key}"); log::error!("Unknown key: {key}");
} }
@@ -199,20 +218,38 @@ where
if label.is_empty() { if label.is_empty() {
label = LAYOUT.label_for_key(key); label = LAYOUT.label_for_key(key);
} }
let button = canvas.key_button(x, y, w, h, 12., cap_type, &label); let _ = panel.layout.add_child(
button.state = Some(state); div,
button.on_press = Some(key_press); Rectangle::create(RectangleParams {
button.on_release = Some(key_release); border_color: parse_color_hex("#dddddd").unwrap(),
button.test_highlight = Some(test_highlight); border: 2.0,
round: WLength::Units(4.0),
..Default::default()
})
.unwrap(),
taffy::Style {
size: my_size,
min_size: my_size,
max_size: my_size,
..Default::default()
},
)?;
} else {
let _ = panel.layout.add_child(
div,
Div::create().unwrap(),
taffy::Style {
size: my_size,
min_size: my_size,
max_size: my_size,
..Default::default()
},
)?;
} }
} }
sum_size += my_size;
} }
} }
let canvas = canvas.build();
let interaction_transform = Affine2::from_translation(vec2(0.5, 0.5)) let interaction_transform = Affine2::from_translation(vec2(0.5, 0.5))
* Affine2::from_scale(vec2(1., -size.x as f32 / size.y as f32)); * Affine2::from_scale(vec2(1., -size.x as f32 / size.y as f32));
@@ -230,120 +267,11 @@ where
interaction_transform, interaction_transform,
..Default::default() ..Default::default()
}, },
backend: Box::new(KeyboardBackend { canvas }), backend: Box::new(KeyboardBackend { panel }),
..Default::default() ..Default::default()
}) })
} }
fn key_press(
control: &mut Control<KeyboardData, KeyButtonData>,
data: &mut KeyboardData,
app: &mut AppState,
mode: PointerMode,
) {
match control.state.as_mut() {
Some(KeyButtonData::Key { vk, pressed }) => {
key_click(app);
data.modifiers |= match mode {
PointerMode::Right => SHIFT,
PointerMode::Middle => data.alt_modifier,
_ => 0,
};
set_modifiers(app, data.modifiers);
send_key(app, *vk, true);
*pressed = true;
}
Some(KeyButtonData::Modifier { modifier, sticky }) => {
*sticky = data.modifiers & *modifier == 0;
data.modifiers |= *modifier;
key_click(app);
set_modifiers(app, data.modifiers);
}
Some(KeyButtonData::Macro { verbs }) => {
key_click(app);
for (vk, press) in verbs {
send_key(app, *vk, *press);
}
}
Some(KeyButtonData::Exec { program, args, .. }) => {
// Reap previous processes
data.processes
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
key_click(app);
if let Ok(child) = Command::new(program).args(args).spawn() {
data.processes.push(child);
}
}
None => {}
}
}
fn key_release(
control: &mut Control<KeyboardData, KeyButtonData>,
data: &mut KeyboardData,
app: &mut AppState,
) {
match control.state.as_mut() {
Some(KeyButtonData::Key { vk, pressed }) => {
send_key(app, *vk, false);
*pressed = false;
for m in &AUTO_RELEASE_MODS {
if data.modifiers & *m != 0 {
data.modifiers &= !*m;
set_modifiers(app, data.modifiers);
}
}
}
Some(KeyButtonData::Modifier { modifier, sticky }) => {
if !*sticky {
data.modifiers &= !*modifier;
set_modifiers(app, data.modifiers);
}
}
Some(KeyButtonData::Exec {
release_program,
release_args,
..
}) => {
// Reap previous processes
data.processes
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
if let Some(program) = release_program {
if let Ok(child) = Command::new(program).args(release_args).spawn() {
data.processes.push(child);
}
}
}
_ => {}
}
}
static PRESS_COLOR: Vec4 = Vec4::new(198. / 255., 160. / 255., 246. / 255., 0.5);
fn test_highlight(
control: &Control<KeyboardData, KeyButtonData>,
data: &mut KeyboardData,
_app: &mut AppState,
) -> Option<Vec4> {
let pressed = match control.state.as_ref() {
Some(KeyButtonData::Key { pressed, .. }) => *pressed,
Some(KeyButtonData::Modifier { modifier, .. }) => data.modifiers & *modifier != 0,
_ => false,
};
if pressed {
Some(PRESS_COLOR)
} else {
None
}
}
struct KeyboardData { struct KeyboardData {
modifiers: KeyModifier, modifiers: KeyModifier,
alt_modifier: KeyModifier, alt_modifier: KeyModifier,
@@ -501,15 +429,15 @@ fn key_events_for_macro(macro_verbs: &Vec<String>) -> Vec<(VirtualKey, bool)> {
} }
struct KeyboardBackend { struct KeyboardBackend {
canvas: Canvas<KeyboardData, KeyButtonData>, panel: GuiPanel,
} }
impl OverlayBackend for KeyboardBackend { impl OverlayBackend for KeyboardBackend {
fn set_interaction(&mut self, interaction: Box<dyn crate::backend::input::InteractionHandler>) { fn set_interaction(&mut self, interaction: Box<dyn crate::backend::input::InteractionHandler>) {
self.canvas.set_interaction(interaction); self.panel.set_interaction(interaction);
} }
fn set_renderer(&mut self, renderer: Box<dyn crate::backend::overlay::OverlayRenderer>) { fn set_renderer(&mut self, renderer: Box<dyn crate::backend::overlay::OverlayRenderer>) {
self.canvas.set_renderer(renderer); self.panel.set_renderer(renderer);
} }
} }
@@ -520,7 +448,7 @@ impl InteractionHandler for KeyboardBackend {
hit: &crate::backend::input::PointerHit, hit: &crate::backend::input::PointerHit,
pressed: bool, pressed: bool,
) { ) {
self.canvas.on_pointer(app, hit, pressed); self.panel.on_pointer(app, hit, pressed);
} }
fn on_scroll( fn on_scroll(
&mut self, &mut self,
@@ -529,26 +457,26 @@ impl InteractionHandler for KeyboardBackend {
delta_y: f32, delta_y: f32,
delta_x: f32, delta_x: f32,
) { ) {
self.canvas.on_scroll(app, hit, delta_y, delta_x); self.panel.on_scroll(app, hit, delta_y, delta_x);
} }
fn on_left(&mut self, app: &mut AppState, pointer: usize) { fn on_left(&mut self, app: &mut AppState, pointer: usize) {
self.canvas.on_left(app, pointer); self.panel.on_left(app, pointer);
} }
fn on_hover( fn on_hover(
&mut self, &mut self,
app: &mut AppState, app: &mut AppState,
hit: &crate::backend::input::PointerHit, hit: &crate::backend::input::PointerHit,
) -> Option<crate::backend::input::Haptics> { ) -> Option<crate::backend::input::Haptics> {
self.canvas.on_hover(app, hit) self.panel.on_hover(app, hit)
} }
} }
impl OverlayRenderer for KeyboardBackend { impl OverlayRenderer for KeyboardBackend {
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> { fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.canvas.init(app) self.panel.init(app)
} }
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> { fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
self.canvas.should_render(app) self.panel.should_render(app)
} }
fn render( fn render(
&mut self, &mut self,
@@ -557,17 +485,31 @@ impl OverlayRenderer for KeyboardBackend {
buf: &mut CommandBuffers, buf: &mut CommandBuffers,
alpha: f32, alpha: f32,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
self.canvas.render(app, tgt, buf, alpha) self.panel.render(app, tgt, buf, alpha)
} }
fn frame_meta(&mut self) -> Option<FrameMeta> { fn frame_meta(&mut self) -> Option<FrameMeta> {
self.canvas.frame_meta() self.panel.frame_meta()
} }
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> { fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.canvas.data_mut().modifiers = 0;
set_modifiers(app, 0); set_modifiers(app, 0);
self.canvas.pause(app) self.panel.pause(app)
} }
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> { fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.canvas.resume(app) self.panel.resume(app)
} }
} }
pub enum KeyCapType {
/// Label is in center of keycap
Regular,
/// Label on the top
/// AltGr symbol on bottom
RegularAltGr,
/// Primary symbol on bottom
/// Shift symbol on top
Reversed,
/// Primary symbol on bottom-left
/// Shift symbol on top-left
/// AltGr symbol on bottom-right
ReversedAltGr,
}

View File

@@ -7,12 +7,14 @@ use std::{
time::Instant, time::Instant,
}; };
use vulkano::{ use vulkano::{
buffer::{BufferUsage, Subbuffer},
command_buffer::CommandBufferUsage, command_buffer::CommandBufferUsage,
device::Queue, device::Queue,
format::Format, format::Format,
image::{sampler::Filter, view::ImageView, Image}, image::{sampler::Filter, view::ImageView, Image},
pipeline::graphics::color_blend::AttachmentBlend, pipeline::graphics::{color_blend::AttachmentBlend, input_assembly::PrimitiveTopology},
}; };
use wgui::gfx::{cmd::XferCommandBuffer, pass::WGfxPass, pipeline::WGfxPipeline, WGfx};
use wlx_capture::frame as wlx_frame; use wlx_capture::frame as wlx_frame;
use wlx_capture::{ use wlx_capture::{
@@ -56,7 +58,10 @@ use crate::{
}, },
}, },
config::{def_pw_tokens, GeneralConfig, PwTokenMap}, config::{def_pw_tokens, GeneralConfig, PwTokenMap},
graphics::{fourcc_to_vk, CommandBuffers, WlxGraphics, WlxPipeline, WlxUploadsBuffer}, graphics::{
dmabuf::{fourcc_to_vk, WGfxDmabuf},
upload_quad_vertices, CommandBuffers, ExtentExt, Vert2Uv,
},
hid::{MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT}, hid::{MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT},
state::{AppSession, AppState, KeyboardFocus, ScreenMeta}, state::{AppSession, AppState, KeyboardFocus, ScreenMeta},
}; };
@@ -153,36 +158,44 @@ impl InteractionHandler for ScreenInteractionHandler {
fn on_left(&mut self, _app: &mut AppState, _hand: usize) {} fn on_left(&mut self, _app: &mut AppState, _hand: usize) {}
} }
#[derive(Clone)] struct MousePass {
pass: WGfxPass<Vert2Uv>,
buf_vert: Subbuffer<[Vert2Uv]>,
}
struct ScreenPipeline { struct ScreenPipeline {
mouse: Option<Arc<ImageView>>, mouse: Option<MousePass>,
pipeline: Arc<WlxPipeline>, pipeline: Arc<WGfxPipeline<Vert2Uv>>,
buf_alpha: Subbuffer<[f32]>,
extentf: [f32; 2], extentf: [f32; 2],
} }
impl ScreenPipeline { impl ScreenPipeline {
fn new(extent: &[u32; 3], app: &mut AppState) -> anyhow::Result<Self> { fn new(extent: &[u32; 3], app: &mut AppState) -> anyhow::Result<Self> {
let Ok(shaders) = app.graphics.shared_shaders.read() else { let pipeline = app.gfx.create_pipeline(
return Err(anyhow::anyhow!("Could not lock shared shaders for reading")); app.gfx_extras.shaders.get("vert_quad").unwrap().clone(), // want panic
}; app.gfx_extras.shaders.get("frag_screen").unwrap().clone(), // want panic
app.gfx.surface_format,
let pipeline = app.graphics.create_pipeline(
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_screen").unwrap().clone(), // want panic
app.graphics.native_format,
Some(AttachmentBlend::default()), Some(AttachmentBlend::default()),
PrimitiveTopology::TriangleStrip,
false,
)?; )?;
let buf_alpha = app
.gfx
.empty_buffer(BufferUsage::TRANSFER_DST | BufferUsage::UNIFORM_BUFFER, 1)?;
let extentf = [extent[0] as f32, extent[1] as f32]; let extentf = [extent[0] as f32, extent[1] as f32];
Ok(Self { Ok(Self {
mouse: None, mouse: None,
pipeline, pipeline,
buf_alpha,
extentf, extentf,
}) })
} }
fn ensure_mouse_initialized(&mut self, uploads: &mut WlxUploadsBuffer) -> anyhow::Result<()> { fn ensure_mouse_initialized(&mut self, cmd_xfer: &mut XferCommandBuffer) -> anyhow::Result<()> {
if self.mouse.is_some() { if self.mouse.is_some() {
return Ok(()); return Ok(());
} }
@@ -195,9 +208,28 @@ impl ScreenPipeline {
0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff,
]; ];
let mouse_tex = let image =
uploads.texture2d_raw(4, 4, vulkano::format::Format::R8G8B8A8_UNORM, &mouse_bytes)?; cmd_xfer.upload_image(4, 4, vulkano::format::Format::R8G8B8A8_UNORM, &mouse_bytes)?;
self.mouse = Some(ImageView::new_default(mouse_tex)?);
let view = ImageView::new_default(image)?;
let buf_vert = cmd_xfer
.graphics
.empty_buffer(BufferUsage::TRANSFER_DST | BufferUsage::VERTEX_BUFFER, 4)?;
let set0 = self.pipeline.uniform_sampler(0, view, Filter::Nearest)?;
let set1 = self.pipeline.buffer(1, self.buf_alpha.clone())?;
let pass = self.pipeline.create_pass(
self.extentf,
buf_vert.clone(),
0..4,
0..1,
vec![set0, set1],
)?;
self.mouse = Some(MousePass { pass, buf_vert });
Ok(()) Ok(())
} }
@@ -213,23 +245,31 @@ impl ScreenPipeline {
let view = ImageView::new_default(image)?; let view = ImageView::new_default(image)?;
let set0 = self let set0 = self
.pipeline .pipeline
.uniform_sampler(0, view, app.graphics.texture_filtering)?; .uniform_sampler(0, view, app.gfx.texture_filter)?;
let set1 = self.pipeline.uniform_buffer(1, vec![alpha])?;
let pass = self self.buf_alpha.write()?[0] = alpha;
.pipeline
.create_pass_for_target(tgt.clone(), vec![set0, set1])?; let set1 = self.pipeline.buffer(1, self.buf_alpha.clone())?;
let pass = self.pipeline.create_pass(
tgt.extent_f32(),
app.gfx_extras.quad_verts.clone(),
0..4,
0..1,
vec![set0, set1],
)?;
let mut cmd = app let mut cmd = app
.graphics .gfx
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?; .create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd.begin_rendering(tgt)?; cmd.begin_rendering(tgt)?;
cmd.run_ref(&pass)?; cmd.run_ref(&pass)?;
if let (Some(mouse), Some(mouse_view)) = (mouse, self.mouse.clone()) { if let (Some(mouse), Some(pass)) = (mouse, self.mouse.as_mut()) {
let size = CURSOR_SIZE * self.extentf[1]; let size = CURSOR_SIZE * self.extentf[1];
let half_size = size * 0.5; let half_size = size * 0.5;
let vertex_buffer = app.graphics.upload_verts( upload_quad_vertices(
&mut pass.buf_vert,
self.extentf[0], self.extentf[0],
self.extentf[1], self.extentf[1],
mouse.x.mul_add(self.extentf[0], -half_size), mouse.x.mul_add(self.extentf[0], -half_size),
@@ -238,20 +278,7 @@ impl ScreenPipeline {
size, size,
)?; )?;
let set0 = self cmd.run_ref(&pass.pass)?;
.pipeline
.uniform_sampler(0, mouse_view, Filter::Nearest)?;
let set1 = self.pipeline.uniform_buffer(1, vec![alpha])?;
let pass = self.pipeline.create_pass(
self.extentf,
vertex_buffer,
app.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd.run_ref(&pass)?;
} }
cmd.end_rendering()?; cmd.end_rendering()?;
@@ -296,7 +323,7 @@ impl ScreenRenderer {
pub fn new_wlr_dmabuf(output: &WlxOutput, app: &AppState) -> Option<Self> { pub fn new_wlr_dmabuf(output: &WlxOutput, app: &AppState) -> Option<Self> {
let client = WlxClient::new()?; let client = WlxClient::new()?;
let capture = new_wlx_capture!( let capture = new_wlx_capture!(
app.graphics.capture_queue, app.gfx_extras.queue_capture,
WlrDmabufCapture::new(client, output.id) WlrDmabufCapture::new(client, output.id)
); );
Some(Self::new_raw(output.name.clone(), capture)) Some(Self::new_raw(output.name.clone(), capture))
@@ -306,7 +333,7 @@ impl ScreenRenderer {
pub fn new_wlr_screencopy(output: &WlxOutput, app: &AppState) -> Option<Self> { pub fn new_wlr_screencopy(output: &WlxOutput, app: &AppState) -> Option<Self> {
let client = WlxClient::new()?; let client = WlxClient::new()?;
let capture = new_wlx_capture!( let capture = new_wlx_capture!(
app.graphics.capture_queue, app.gfx_extras.queue_capture,
WlrScreencopyCapture::new(client, output.id) WlrScreencopyCapture::new(client, output.id)
); );
Some(Self::new_raw(output.name.clone(), capture)) Some(Self::new_raw(output.name.clone(), capture))
@@ -340,7 +367,7 @@ impl ScreenRenderer {
let node_id = select_screen_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element let node_id = select_screen_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
let capture = new_wlx_capture!( let capture = new_wlx_capture!(
app.graphics.capture_queue, app.gfx_extras.queue_capture,
PipewireCapture::new(name, node_id) PipewireCapture::new(name, node_id)
); );
Ok(( Ok((
@@ -351,8 +378,10 @@ impl ScreenRenderer {
#[cfg(feature = "x11")] #[cfg(feature = "x11")]
pub fn new_xshm(screen: Arc<XshmScreen>, app: &AppState) -> Self { pub fn new_xshm(screen: Arc<XshmScreen>, app: &AppState) -> Self {
let capture = let capture = new_wlx_capture!(
new_wlx_capture!(app.graphics.capture_queue, XshmCapture::new(screen.clone())); app.gfx_extras.queue_capture,
XshmCapture::new(screen.clone())
);
Self::new_raw(screen.name.clone(), capture) Self::new_raw(screen.name.clone(), capture)
} }
} }
@@ -360,7 +389,7 @@ impl ScreenRenderer {
#[derive(Clone)] #[derive(Clone)]
pub struct WlxCaptureIn { pub struct WlxCaptureIn {
name: Arc<str>, name: Arc<str>,
graphics: Arc<WlxGraphics>, gfx: Arc<WGfx>,
queue: Arc<Queue>, queue: Arc<Queue>,
} }
@@ -378,9 +407,9 @@ fn upload_image(
format: Format, format: Format,
data: &[u8], data: &[u8],
) -> Option<Arc<Image>> { ) -> Option<Arc<Image>> {
let mut upload = match me let mut cmd_xfer = match me
.graphics .gfx
.create_uploads_command_buffer(me.queue.clone(), CommandBufferUsage::OneTimeSubmit) .create_xfer_command_buffer_with_queue(me.queue.clone(), CommandBufferUsage::OneTimeSubmit)
{ {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
@@ -388,7 +417,7 @@ fn upload_image(
return None; return None;
} }
}; };
let image = match upload.texture2d_raw(width, height, format, data) { let image = match cmd_xfer.upload_image(width, height, format, data) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
log::error!("{}: Could not create vkImage: {:?}", me.name, e); log::error!("{}: Could not create vkImage: {:?}", me.name, e);
@@ -396,7 +425,7 @@ fn upload_image(
} }
}; };
if let Err(e) = upload.build_and_execute_now() { if let Err(e) = cmd_xfer.build_and_execute_now() {
log::error!("{}: Could not execute upload: {:?}", me.name, e); log::error!("{}: Could not execute upload: {:?}", me.name, e);
return None; return None;
} }
@@ -413,7 +442,7 @@ fn receive_callback(me: &WlxCaptureIn, frame: wlx_frame::WlxFrame) -> Option<Wlx
} }
log::trace!("{}: New DMA-buf frame", me.name); log::trace!("{}: New DMA-buf frame", me.name);
let format = frame.format; let format = frame.format;
match me.graphics.dmabuf_texture(frame) { match me.gfx.dmabuf_texture(frame) {
Ok(image) => Some(WlxCaptureOut { Ok(image) => Some(WlxCaptureOut {
image, image,
format, format,
@@ -499,7 +528,7 @@ impl OverlayRenderer for ScreenRenderer {
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> { fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
if !self.capture.is_ready() { if !self.capture.is_ready() {
let supports_dmabuf = app let supports_dmabuf = app
.graphics .gfx
.device .device
.enabled_extensions() .enabled_extensions()
.ext_external_memory_dma_buf .ext_external_memory_dma_buf
@@ -512,13 +541,13 @@ impl OverlayRenderer for ScreenRenderer {
let dmabuf_formats = if !supports_dmabuf { let dmabuf_formats = if !supports_dmabuf {
log::info!("Capture method does not support DMA-buf"); log::info!("Capture method does not support DMA-buf");
if app.graphics.capture_queue.is_none() { if app.gfx_extras.queue_capture.is_none() {
log::warn!("Current GPU does not support multiple queues. Software capture will take place on the main thread. Expect degraded performance."); log::warn!("Current GPU does not support multiple queues. Software capture will take place on the main thread. Expect degraded performance.");
} }
&Vec::new() &Vec::new()
} else if !allow_dmabuf { } else if !allow_dmabuf {
log::info!("Not using DMA-buf capture due to {capture_method}"); log::info!("Not using DMA-buf capture due to {capture_method}");
if app.graphics.capture_queue.is_none() { if app.gfx_extras.queue_capture.is_none() {
log::warn!("Current GPU does not support multiple queues. Software capture will take place on the main thread. Expect degraded performance."); log::warn!("Current GPU does not support multiple queues. Software capture will take place on the main thread. Expect degraded performance.");
} }
&Vec::new() &Vec::new()
@@ -528,17 +557,17 @@ impl OverlayRenderer for ScreenRenderer {
); );
log::warn!("echo 'capture_method: pw_fallback' > ~/.config/wlxoverlay/conf.d/pw_fallback.yaml"); log::warn!("echo 'capture_method: pw_fallback' > ~/.config/wlxoverlay/conf.d/pw_fallback.yaml");
&app.graphics.drm_formats &app.gfx_extras.drm_formats
}; };
let user_data = WlxCaptureIn { let user_data = WlxCaptureIn {
name: self.name.clone(), name: self.name.clone(),
graphics: app.graphics.clone(), gfx: app.gfx.clone(),
queue: app queue: app
.graphics .gfx_extras
.capture_queue .queue_capture
.as_ref() .as_ref()
.unwrap_or_else(|| &app.graphics.transfer_queue) .unwrap_or_else(|| &app.gfx.queue_xfer)
.clone(), .clone(),
}; };
@@ -560,10 +589,9 @@ impl OverlayRenderer for ScreenRenderer {
if let (Some(capture), None) = (self.cur_frame.as_ref(), self.pipeline.as_ref()) { if let (Some(capture), None) = (self.cur_frame.as_ref(), self.pipeline.as_ref()) {
self.pipeline = Some({ self.pipeline = Some({
let mut pipeline = ScreenPipeline::new(&capture.image.extent(), app)?; let mut pipeline = ScreenPipeline::new(&capture.image.extent(), app)?;
let mut upload = app.graphics.create_uploads_command_buffer( let mut upload = app
app.graphics.transfer_queue.clone(), .gfx
CommandBufferUsage::OneTimeSubmit, .create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
)?;
pipeline.ensure_mouse_initialized(&mut upload)?; pipeline.ensure_mouse_initialized(&mut upload)?;
upload.build_and_execute_now()?; upload.build_and_execute_now()?;
pipeline pipeline
@@ -931,7 +959,7 @@ pub fn create_screens_x11pw(app: &mut AppState) -> anyhow::Result<ScreenCreateDa
let renderer = ScreenRenderer::new_raw( let renderer = ScreenRenderer::new_raw(
m.name.clone(), m.name.clone(),
new_wlx_capture!( new_wlx_capture!(
app.graphics.capture_queue, app.gfx_extras.queue_capture,
PipewireCapture::new(m.name.clone(), s.node_id) PipewireCapture::new(m.name.clone(), s.node_id)
), ),
); );

View File

@@ -8,6 +8,19 @@ use std::{
use glam::{vec3a, Quat}; use glam::{vec3a, Quat};
use idmap_derive::IntegerId; use idmap_derive::IntegerId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wgui::{
parser::parse_color_hex,
renderer_vk::text::{FontWeight, TextStyle},
taffy::{
self,
prelude::{auto, length, percent},
},
widget::{
rectangle::{Rectangle, RectangleParams},
text::{TextLabel, TextParams},
util::WLength,
},
};
use crate::{ use crate::{
backend::{ backend::{
@@ -15,7 +28,7 @@ use crate::{
overlay::{OverlayBackend, OverlayState, Positioning, Z_ORDER_TOAST}, overlay::{OverlayBackend, OverlayState, Positioning, Z_ORDER_TOAST},
task::TaskType, task::TaskType,
}, },
gui::{canvas::builder::CanvasBuilder, color_parse}, gui::panel::GuiPanel,
state::{AppState, LeftRight}, state::{AppState, LeftRight},
}; };
@@ -40,8 +53,8 @@ pub enum ToastTopic {
} }
pub struct Toast { pub struct Toast {
pub title: Arc<str>, pub title: String,
pub body: Arc<str>, pub body: String,
pub opacity: f32, pub opacity: f32,
pub timeout: f32, pub timeout: f32,
pub sound: bool, pub sound: bool,
@@ -50,7 +63,7 @@ pub struct Toast {
#[allow(dead_code)] #[allow(dead_code)]
impl Toast { impl Toast {
pub const fn new(topic: ToastTopic, title: Arc<str>, body: Arc<str>) -> Self { pub const fn new(topic: ToastTopic, title: String, body: String) -> Self {
Self { Self {
title, title,
body, body,
@@ -117,6 +130,7 @@ impl Toast {
} }
} }
#[allow(clippy::too_many_lines)]
fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn OverlayBackend>)> { fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn OverlayBackend>)> {
let current_method = app let current_method = app
.session .session
@@ -153,63 +167,79 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
toast.title toast.title
}; };
let mut size = if toast.body.is_empty() { let mut panel = GuiPanel::new_blank(app, 600, 200).ok()?;
let (w, h) = app
.fc
.get_text_size(&title, FONT_SIZE, app.graphics.clone())
.ok()?;
(w, h + 20.)
} else {
let (w0, _) = app
.fc
.get_text_size(&title, FONT_SIZE, app.graphics.clone())
.ok()?;
let (w1, h1) = app
.fc
.get_text_size(&toast.body, FONT_SIZE, app.graphics.clone())
.ok()?;
(w0.max(w1), h1 + 50.)
};
let og_width = size.0; let (rect, _) = panel
size.0 += PADDING.0 * 2.; .layout
.add_child(
let mut canvas = CanvasBuilder::<(), ()>::new( panel.layout.root_widget,
size.0 as _, Rectangle::create(RectangleParams {
size.1 as _, color: parse_color_hex("#1e2030").unwrap(),
app.graphics.clone(), border_color: parse_color_hex("#5e7090").unwrap(),
app.graphics.native_format, 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),
padding: length(1.0),
..Default::default()
},
) )
.ok()?; .ok()?;
canvas.font_size = FONT_SIZE; let _ = panel.layout.add_child(
canvas.fg_color = color_parse("#cad3f5").unwrap(); // want panic rect,
canvas.bg_color = color_parse("#1e2030").unwrap(); // want panic TextLabel::create(TextParams {
canvas.panel(0., 0., size.0, size.1, 16.); content: title,
style: TextStyle {
color: parse_color_hex("#ffffff"),
..Default::default()
},
})
.unwrap(),
taffy::Style {
size: taffy::Size {
width: percent(1.0),
height: auto(),
},
..Default::default()
},
);
if toast.body.is_empty() { let _ = panel.layout.add_child(
canvas.label_centered(PADDING.0, 0., og_width, size.1, 16., title); rect,
} else { TextLabel::create(TextParams {
canvas.label(PADDING.0, 54., og_width, size.1 - 54., 3., toast.body); content: toast.body,
style: TextStyle {
canvas.fg_color = color_parse("#b8c0e0").unwrap(); // want panic weight: Some(FontWeight::Bold),
canvas.bg_color = color_parse("#24273a").unwrap(); // want panic color: parse_color_hex("#eeeeee"),
canvas.panel(0., 0., size.0, 30., 16.); ..Default::default()
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., 16., title); },
} })
.unwrap(),
taffy::Style {
size: taffy::Size {
width: percent(1.0),
height: auto(),
},
..Default::default()
},
);
let state = OverlayState { let state = OverlayState {
name: TOAST_NAME.clone(), name: TOAST_NAME.clone(),
want_visible: true, want_visible: true,
spawn_scale: size.0 * PIXELS_TO_METERS, spawn_scale: (panel.width as f32) * PIXELS_TO_METERS,
spawn_rotation, spawn_rotation,
spawn_point, spawn_point,
z_order: Z_ORDER_TOAST, z_order: Z_ORDER_TOAST,
positioning, positioning,
..Default::default() ..Default::default()
}; };
let backend = Box::new(canvas.build()); let backend = Box::new(panel);
Some((state, backend)) Some((state, backend))
} }

View File

@@ -1,25 +1,54 @@
use glam::Vec3A; use glam::Vec3A;
use wgui::{
parser::parse_color_hex,
taffy::{
self,
prelude::{length, percent},
},
widget::{
rectangle::{Rectangle, RectangleParams},
util::WLength,
},
};
use crate::{ use crate::{
backend::overlay::{ui_transform, OverlayData, OverlayState, Positioning, Z_ORDER_WATCH}, backend::overlay::{ui_transform, OverlayData, OverlayState, Positioning, Z_ORDER_WATCH},
config::{load_known_yaml, ConfigType}, gui::panel::GuiPanel,
gui::{
canvas::Canvas,
modular::{modular_canvas, ModularData, ModularUiConfig},
},
state::AppState, state::AppState,
}; };
pub const WATCH_NAME: &str = "watch"; pub const WATCH_NAME: &str = "watch";
pub fn create_watch<O>(state: &mut AppState) -> anyhow::Result<OverlayData<O>> pub fn create_watch<O>(app: &mut AppState) -> anyhow::Result<OverlayData<O>>
where where
O: Default, O: Default,
{ {
let config = load_known_yaml::<ModularUiConfig>(ConfigType::Watch); let mut panel = GuiPanel::new_blank(app, 400, 200)?;
let (_, _) = panel.layout.add_child(
panel.layout.root_widget,
Rectangle::create(RectangleParams {
color: wgui::drawing::Color::new(0., 0., 0., 0.5),
border_color: parse_color_hex("#00ffff").unwrap(),
border: 2.0,
round: WLength::Units(4.0),
..Default::default()
})
.unwrap(),
taffy::Style {
size: taffy::Size {
width: percent(1.0),
height: percent(1.0),
},
align_items: Some(taffy::AlignItems::Center),
justify_content: Some(taffy::JustifyContent::Center),
padding: length(4.0),
..Default::default()
},
)?;
let positioning = Positioning::FollowHand { let positioning = Positioning::FollowHand {
hand: state.session.config.watch_hand as _, hand: app.session.config.watch_hand as _,
lerp: 1.0, lerp: 1.0,
}; };
@@ -29,27 +58,18 @@ where
want_visible: true, want_visible: true,
interactable: true, interactable: true,
z_order: Z_ORDER_WATCH, z_order: Z_ORDER_WATCH,
spawn_scale: config.width, spawn_scale: 0.115, //TODO:configurable
spawn_point: state.session.config.watch_pos, spawn_point: app.session.config.watch_pos,
spawn_rotation: state.session.config.watch_rot, spawn_rotation: app.session.config.watch_rot,
interaction_transform: ui_transform(config.size), interaction_transform: ui_transform([400, 200]),
positioning, positioning,
..Default::default() ..Default::default()
}, },
backend: Box::new(create_watch_canvas(Some(config), state)?), backend: Box::new(panel),
..Default::default() ..Default::default()
}) })
} }
pub fn create_watch_canvas(
config: Option<ModularUiConfig>,
state: &mut AppState,
) -> anyhow::Result<Canvas<(), ModularData>> {
let config = config.unwrap_or_else(|| load_known_yaml::<ModularUiConfig>(ConfigType::Watch));
modular_canvas(config.size, &config.elements, state)
}
pub fn watch_fade<D>(app: &mut AppState, watch: &mut OverlayData<D>) pub fn watch_fade<D>(app: &mut AppState, watch: &mut OverlayData<D>)
where where
D: Default, D: Default,

View File

@@ -2,9 +2,12 @@ use glam::{vec3a, Affine2, Vec3, Vec3A};
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use vulkano::{ use vulkano::{
command_buffer::CommandBufferUsage, command_buffer::CommandBufferUsage,
image::{view::ImageView, SubresourceLayout}, format::Format,
image::{view::ImageView, Image, ImageTiling, SubresourceLayout},
pipeline::graphics::input_assembly::PrimitiveTopology,
}; };
use wayvr_ipc::packet_server::{self, PacketServer, WvrStateChanged}; use wayvr_ipc::packet_server::{self, PacketServer, WvrStateChanged};
use wgui::gfx::{pipeline::WGfxPipeline, WGfx};
use wlx_capture::frame::{DmabufFrame, FourCC, FrameFormat, FramePlane}; use wlx_capture::frame::{DmabufFrame, FourCC, FrameFormat, FramePlane};
use crate::{ use crate::{
@@ -19,12 +22,11 @@ use crate::{
wayvr::{ wayvr::{
self, display, self, display,
server_ipc::{gen_args_vec, gen_env_vec}, server_ipc::{gen_args_vec, gen_env_vec},
WayVR, WayVR, WayVRAction, WayVRDisplayClickAction,
}, },
}, },
config_wayvr, config_wayvr,
graphics::{CommandBuffers, WlxGraphics, WlxPipeline}, graphics::{dmabuf::WGfxDmabuf, CommandBuffers, ExtentExt, Vert2Uv},
gui::modular::button::{WayVRAction, WayVRDisplayClickAction},
state::{self, AppState, KeyboardFocus}, state::{self, AppState, KeyboardFocus},
}; };
@@ -175,15 +177,15 @@ impl InteractionHandler for WayVRInteractionHandler {
} }
struct ImageData { struct ImageData {
vk_image: Arc<vulkano::image::Image>, vk_image: Arc<Image>,
vk_image_view: Arc<vulkano::image::view::ImageView>, vk_image_view: Arc<ImageView>,
} }
pub struct WayVRRenderer { pub struct WayVRRenderer {
pipeline: Arc<WlxPipeline>, pipeline: Arc<WGfxPipeline<Vert2Uv>>,
image: Option<ImageData>, image: Option<ImageData>,
context: Rc<RefCell<WayVRContext>>, context: Rc<RefCell<WayVRContext>>,
graphics: Arc<WlxGraphics>, graphics: Arc<WGfx>,
resolution: [u16; 2], resolution: [u16; 2],
} }
@@ -194,21 +196,19 @@ impl WayVRRenderer {
display: wayvr::display::DisplayHandle, display: wayvr::display::DisplayHandle,
resolution: [u16; 2], resolution: [u16; 2],
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
let Ok(shaders) = app.graphics.shared_shaders.read() else { let pipeline = app.gfx.create_pipeline(
anyhow::bail!("Failed to lock shared shaders for reading"); app.gfx_extras.shaders.get("vert_quad").unwrap().clone(), // want panic
}; app.gfx_extras.shaders.get("frag_srgb").unwrap().clone(), // want panic
app.gfx.surface_format,
let pipeline = app.graphics.create_pipeline(
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_srgb").unwrap().clone(), // want panic
app.graphics.native_format,
None, None,
PrimitiveTopology::TriangleStrip,
false,
)?; )?;
Ok(Self { Ok(Self {
pipeline, pipeline,
context: Rc::new(RefCell::new(WayVRContext::new(wvr, display))), context: Rc::new(RefCell::new(WayVRContext::new(wvr, display))),
graphics: app.graphics.clone(), graphics: app.gfx.clone(),
image: None, image: None,
resolution, resolution,
}) })
@@ -574,15 +574,14 @@ impl WayVRRenderer {
&mut self, &mut self,
data: &wayvr::egl_data::RenderSoftwarePixelsData, data: &wayvr::egl_data::RenderSoftwarePixelsData,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut upload = self.graphics.create_uploads_command_buffer( let mut upload = self
self.graphics.transfer_queue.clone(), .graphics
CommandBufferUsage::OneTimeSubmit, .create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
)?;
let tex = upload.texture2d_raw( let tex = upload.upload_image(
u32::from(data.width), u32::from(data.width),
u32::from(data.height), u32::from(data.height),
vulkano::format::Format::R8G8B8A8_UNORM, Format::R8G8B8A8_UNORM,
&data.data, &data.data,
)?; )?;
@@ -644,7 +643,7 @@ impl WayVRRenderer {
let tex = self.graphics.dmabuf_texture_ex( let tex = self.graphics.dmabuf_texture_ex(
frame, frame,
vulkano::image::ImageTiling::DrmFormatModifier, ImageTiling::DrmFormatModifier,
layouts, layouts,
&data.mod_info.modifiers, &data.mod_info.modifiers,
)?; )?;
@@ -732,18 +731,22 @@ impl OverlayRenderer for WayVRRenderer {
let set0 = self.pipeline.uniform_sampler( let set0 = self.pipeline.uniform_sampler(
0, 0,
image.vk_image_view.clone(), image.vk_image_view.clone(),
app.graphics.texture_filtering, app.gfx.texture_filter,
)?; )?;
let set1 = self.pipeline.uniform_buffer(1, vec![alpha])?; let set1 = self.pipeline.uniform_buffer_upload(1, vec![alpha])?;
let pass = self let pass = self.pipeline.create_pass(
.pipeline tgt.extent_f32(),
.create_pass_for_target(tgt.clone(), vec![set0, set1])?; app.gfx_extras.quad_verts.clone(),
0..4,
0..1,
vec![set0, set1],
)?;
let mut cmd_buffer = app let mut cmd_buffer = app
.graphics .gfx
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?; .create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_rendering(tgt)?; cmd_buffer.begin_rendering(tgt)?;
cmd_buffer.run_ref(&pass)?; cmd_buffer.run_ref(&pass)?;
cmd_buffer.end_rendering()?; cmd_buffer.end_rendering()?;

24
src/shaders/color.frag Normal file
View File

@@ -0,0 +1,24 @@
#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform ColorBlock {
uniform vec4 in_color;
uniform vec2 corner_radius;
};
void main()
{
out_color.r = corner_radius.r;
out_color = in_color;
vec2 uv_circ = ((1. - corner_radius) - (abs(in_uv + vec2(-0.5)) * 2.))/corner_radius;
float dist = length(uv_circ);
out_color.a = mix(out_color.a, 0.,
float(dist > 1.)
* float(uv_circ.x < 0.)
* float(uv_circ.y < 0.));
}

19
src/shaders/grid.frag Normal file
View File

@@ -0,0 +1,19 @@
#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
void main()
{
float fade = max(1.0 - 2.0 * length(in_uv.xy + vec2(-0.5, -0.5)), 0.0);
float grid;
if (fract(in_uv.x / 0.0005) < 0.01 || fract(in_uv.y / 0.0005) < 0.01) {
grid = 1.0;
} else {
grid = 0.0;
}
out_color = vec4(1.0, 1.0, 1.0, grid * fade);
}

View File

@@ -1,266 +1,34 @@
pub mod vert_common { pub mod vert_quad {
vulkano_shaders::shader! { vulkano_shaders::shader! {
ty: "vertex", ty: "vertex",
src: r"#version 310 es path: "src/shaders/quad.vert"
precision highp float;
layout (location = 0) in vec2 in_pos;
layout (location = 1) in vec2 in_uv;
layout (location = 0) out vec2 out_uv;
void main() {
out_uv = in_uv;
gl_Position = vec4(in_pos * 2. - 1., 0., 1.);
}
",
} }
} }
pub mod frag_color { pub mod frag_color {
vulkano_shaders::shader! { vulkano_shaders::shader! {
ty: "fragment", ty: "fragment",
src: r"#version 310 es path: "src/shaders/color.frag",
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform ColorBlock {
uniform vec4 in_color;
uniform vec2 corner_radius;
};
void main()
{
out_color.r = corner_radius.r;
out_color = in_color;
vec2 uv_circ = ((1. - corner_radius) - (abs(in_uv + vec2(-0.5)) * 2.))/corner_radius;
float dist = length(uv_circ);
out_color.a = mix(out_color.a, 0.,
float(dist > 1.)
* float(uv_circ.x < 0.)
* float(uv_circ.y < 0.));
}
",
}
}
//layout (location = 1) in float corner_radius;
//out_color = in_color;
// Some equation that determines whether to keep the pixel
// Use Lerp not if
//out_color.a = 0;
pub mod frag_glyph {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform ColorBlock {
uniform vec4 in_color;
};
void main()
{
float r = texture(in_texture, in_uv).r;
out_color = vec4(r,r,r,r) * in_color;
}
",
}
}
pub mod frag_sprite2 {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform UniBlock {
uniform vec4 st;
uniform vec4 mul;
};
void main()
{
out_color = texture(in_texture, (in_uv * st.xy) + st.zw) * mul;
}
",
}
}
pub mod frag_sprite2_hl {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform UniBlock {
uniform vec4 st;
uniform vec4 mul;
};
void main()
{
out_color = texture(in_texture, (in_uv * st.xy) + st.zw).a * mul;
}
",
}
}
pub mod frag_sprite {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
void main()
{
out_color = texture(in_texture, in_uv);
}
",
} }
} }
pub mod frag_grid { pub mod frag_grid {
vulkano_shaders::shader! { vulkano_shaders::shader! {
ty: "fragment", ty: "fragment",
src: r"#version 310 es path: "src/shaders/grid.frag",
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
void main()
{
float fade = max(1.0 - 2.0 * length(in_uv.xy + vec2(-0.5, -0.5)), 0.0);
float grid;
if (fract(in_uv.x / 0.0005) < 0.01 || fract(in_uv.y / 0.0005) < 0.01) {
grid = 1.0;
} else {
grid = 0.0;
}
out_color = vec4(1.0, 1.0, 1.0, grid * fade);
}
",
} }
} }
pub mod frag_screen { pub mod frag_screen {
vulkano_shaders::shader! { vulkano_shaders::shader! {
ty: "fragment", ty: "fragment",
src: r"#version 310 es path: "src/shaders/screen.frag",
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform AlphaBlock {
uniform float alpha;
};
void main()
{
out_color = texture(in_texture, in_uv);
out_color.a = alpha;
}
",
} }
} }
pub mod frag_srgb { pub mod frag_srgb {
vulkano_shaders::shader! { vulkano_shaders::shader! {
ty: "fragment", ty: "fragment",
src: r"#version 310 es path: "src/shaders/srgb.frag",
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform AlphaBlock {
uniform float alpha;
};
void main()
{
out_color = texture(in_texture, in_uv);
bvec4 cutoff = lessThan(out_color, vec4(0.04045));
vec4 higher = pow((out_color + vec4(0.055))/vec4(1.055), vec4(2.4));
vec4 lower = out_color/vec4(12.92);
out_color = mix(higher, lower, cutoff);
out_color.a *= alpha;
}
",
}
}
pub mod frag_swapchain {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform AlphaBlock {
uniform float alpha;
};
void main()
{
out_color = texture(in_texture, in_uv);
out_color.a *= alpha;
}
",
}
}
pub mod frag_line {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform ColorBlock {
uniform vec4 in_color;
uniform vec2 unused;
};
void main()
{
out_color = in_color;
}
",
} }
} }

11
src/shaders/quad.vert Normal file
View File

@@ -0,0 +1,11 @@
#version 310 es
precision highp float;
layout (location = 0) in vec2 in_pos;
layout (location = 1) in vec2 in_uv;
layout (location = 0) out vec2 out_uv;
void main() {
out_uv = in_uv;
gl_Position = vec4(in_pos * 2. - 1., 0., 1.);
}

17
src/shaders/screen.frag Normal file
View File

@@ -0,0 +1,17 @@
#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform AlphaBlock {
uniform float alpha;
};
void main()
{
out_color = texture(in_texture, in_uv);
out_color.a = alpha;
}

24
src/shaders/srgb.frag Normal file
View File

@@ -0,0 +1,24 @@
#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform AlphaBlock {
uniform float alpha;
};
void main()
{
out_color = texture(in_texture, in_uv);
bvec4 cutoff = lessThan(out_color, vec4(0.04045));
vec4 higher = pow((out_color + vec4(0.055))/vec4(1.055), vec4(2.4));
vec4 lower = out_color/vec4(12.92);
out_color = mix(higher, lower, cutoff);
out_color.a *= alpha;
}

View File

@@ -1,4 +1,3 @@
use anyhow::bail;
use glam::Affine3A; use glam::Affine3A;
use idmap::IdMap; use idmap::IdMap;
use rodio::{Decoder, OutputStream, OutputStreamHandle, Source}; use rodio::{Decoder, OutputStream, OutputStreamHandle, Source};
@@ -6,6 +5,7 @@ use serde::{Deserialize, Serialize};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use std::{io::Cursor, sync::Arc}; use std::{io::Cursor, sync::Arc};
use vulkano::image::view::ImageView; use vulkano::image::view::ImageView;
use wgui::gfx::WGfx;
#[cfg(feature = "wayvr")] #[cfg(feature = "wayvr")]
use { use {
@@ -21,14 +21,9 @@ use crate::{
backend::{input::InputState, overlay::OverlayID, task::TaskContainer}, backend::{input::InputState, overlay::OverlayID, task::TaskContainer},
config::{AStrMap, GeneralConfig}, config::{AStrMap, GeneralConfig},
config_io, config_io,
graphics::WlxGraphics, graphics::WGfxExtras,
gui::font::FontCache,
hid::HidProvider, hid::HidProvider,
overlays::toast::{DisplayMethod, ToastTopic}, overlays::toast::{DisplayMethod, ToastTopic},
shaders::{
frag_color, frag_glyph, frag_grid, frag_line, frag_screen, frag_sprite, frag_sprite2,
frag_sprite2_hl, frag_srgb, frag_swapchain, vert_common,
},
}; };
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -40,10 +35,12 @@ pub enum KeyboardFocus {
} }
pub struct AppState { pub struct AppState {
pub fc: FontCache,
pub session: AppSession, pub session: AppSession,
pub tasks: TaskContainer, pub tasks: TaskContainer,
pub graphics: Arc<WlxGraphics>,
pub gfx: Arc<WGfx>,
pub gfx_extras: WGfxExtras,
pub input_state: InputState, pub input_state: InputState,
pub hid_provider: Box<dyn HidProvider>, pub hid_provider: Box<dyn HidProvider>,
pub audio: AudioOutput, pub audio: AudioOutput,
@@ -61,47 +58,8 @@ pub struct AppState {
} }
impl AppState { impl AppState {
pub fn from_graphics(graphics: Arc<WlxGraphics>) -> anyhow::Result<Self> { pub fn from_graphics(gfx: Arc<WGfx>, gfx_extras: WGfxExtras) -> anyhow::Result<Self> {
// insert shared resources // insert shared resources
{
let Ok(mut shaders) = graphics.shared_shaders.write() else {
bail!("Failed to lock shared shaders");
};
let shader = vert_common::load(graphics.device.clone())?;
shaders.insert("vert_common", shader);
let shader = frag_color::load(graphics.device.clone())?;
shaders.insert("frag_color", shader);
let shader = frag_line::load(graphics.device.clone())?;
shaders.insert("frag_line", shader);
let shader = frag_srgb::load(graphics.device.clone())?;
shaders.insert("frag_srgb", shader);
let shader = frag_glyph::load(graphics.device.clone())?;
shaders.insert("frag_glyph", shader);
let shader = frag_grid::load(graphics.device.clone())?;
shaders.insert("frag_grid", shader);
let shader = frag_sprite::load(graphics.device.clone())?;
shaders.insert("frag_sprite", shader);
let shader = frag_sprite2::load(graphics.device.clone())?;
shaders.insert("frag_sprite2", shader);
let shader = frag_sprite2_hl::load(graphics.device.clone())?;
shaders.insert("frag_sprite2_hl", shader);
let shader = frag_screen::load(graphics.device.clone())?;
shaders.insert("frag_screen", shader);
let shader = frag_swapchain::load(graphics.device.clone())?;
shaders.insert("frag_swapchain", shader);
}
#[cfg(feature = "wayvr")] #[cfg(feature = "wayvr")]
let mut tasks = TaskContainer::new(); let mut tasks = TaskContainer::new();
@@ -124,10 +82,10 @@ impl AppState {
); );
Ok(Self { Ok(Self {
fc: FontCache::new(session.config.primary_font.clone())?,
session, session,
tasks, tasks,
graphics, gfx,
gfx_extras,
input_state: InputState::new(), input_state: InputState::new(),
hid_provider: crate::hid::initialize(), hid_provider: crate::hid::initialize(),
audio: AudioOutput::new(), audio: AudioOutput::new(),