Add crop rect support for focused screens

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Taylor Beeston 2026-04-11 15:11:43 -07:00
parent 18f99b5daf
commit 40128a6d75
4 changed files with 142 additions and 22 deletions

View File

@ -57,11 +57,14 @@ pub struct ScreenBackend {
stereo: Option<StereoMode>,
stereo_full_frame: bool,
stereo_adjust_mouse: bool,
crop_rect: [f32; 4],
crop_dirty: bool,
pub(super) logical_pos: Vec2,
pub(super) logical_size: Vec2,
pub(super) mouse_transform_original: Transform,
mouse_transform_override: MouseTransform,
just_resumed: bool,
source_meta: Option<FrameMeta>,
}
impl ScreenBackend {
@ -87,17 +90,22 @@ impl ScreenBackend {
},
stereo_full_frame: false,
stereo_adjust_mouse: false,
crop_rect: [0.0, 0.0, 1.0, 1.0],
crop_dirty: false,
logical_pos: Vec2::ZERO,
logical_size: Vec2::ZERO,
mouse_transform_original: Transform::Undefined,
mouse_transform_override: MouseTransform::Default,
just_resumed: false,
source_meta: None,
}
}
pub(super) fn apply_mouse_transform_with_override(&mut self, override_transform: Transform) {
let mut size = self.logical_size;
let pos = self.logical_pos;
let crop_origin = Vec2::new(self.crop_rect[0], self.crop_rect[1]);
let crop_size = Vec2::new(self.crop_rect[2], self.crop_rect[3]);
let mut size = self.logical_size * crop_size;
let pos = self.logical_pos + self.logical_size * crop_origin;
if self.stereo_adjust_mouse
&& let Some(stereo) = self.stereo.as_ref()
@ -153,6 +161,64 @@ impl ScreenBackend {
),
};
}
fn effective_extent(&self, source_extent: [u32; 2]) -> [u32; 2] {
[
(source_extent[0] as f32 * self.crop_rect[2])
.round()
.max(1.0) as u32,
(source_extent[1] as f32 * self.crop_rect[3])
.round()
.max(1.0) as u32,
]
}
fn apply_meta_state(
&mut self,
app: &mut AppState,
mut source_meta: FrameMeta,
stereo: StereoMode,
) -> anyhow::Result<()> {
let source_extent = source_meta.extent;
let effective_extent = self.effective_extent(source_extent);
source_meta.extent = effective_extent;
if let Some(pipeline) = self.pipeline.as_mut() {
if self
.meta
.is_some_and(|old| old.extent != source_meta.extent)
{
pipeline.set_extent(
app,
[source_meta.extent[0] as f32, source_meta.extent[1] as f32],
[0.0, 0.0],
[source_extent[0] as f32, source_extent[1] as f32],
)?;
self.interaction_transform = Some(ui_transform(source_meta.extent));
}
if self.crop_dirty {
pipeline.set_crop_rect(self.crop_rect)?;
}
pipeline.ensure_stereo(stereo);
} else {
let pipeline = ScreenPipeline::new(
&source_meta,
source_extent,
app,
stereo,
[0.0, 0.0],
self.crop_rect,
)?;
self.pipeline = Some(pipeline);
self.interaction_transform = Some(ui_transform(source_meta.extent));
}
self.meta = Some(source_meta);
self.crop_dirty = false;
let frame_transform = mouse_transform_to_transform(self.mouse_transform_override);
self.apply_mouse_transform_with_override(frame_transform);
Ok(())
}
}
impl OverlayBackend for ScreenBackend {
@ -237,26 +303,19 @@ impl OverlayBackend for ScreenBackend {
}
}
if let Some(pipeline) = self.pipeline.as_mut() {
if self.meta.is_some_and(|old| old.extent != meta.extent) {
pipeline.set_extent(
app,
[meta.extent[0] as _, meta.extent[1] as _],
[0., 0.],
)?;
self.interaction_transform = Some(ui_transform(meta.extent));
}
} else {
let pipeline = ScreenPipeline::new(&meta, app, stereo, [0., 0.])?;
self.pipeline = Some(pipeline);
self.interaction_transform = Some(ui_transform(meta.extent));
}
self.meta = Some(meta);
self.source_meta = Some(meta);
self.apply_meta_state(app, meta, stereo)?;
self.cur_frame = Some(frame);
Ok(ShouldRender::Should)
} else if self.cur_frame.is_some() {
if self.crop_dirty
&& let Some(source_meta) = self.source_meta
{
let stereo = self.stereo.unwrap_or(StereoMode::None);
self.apply_meta_state(app, source_meta, stereo)?;
return Ok(ShouldRender::Should);
}
if self.just_resumed {
self.just_resumed = false;
Ok(ShouldRender::Should)
@ -364,6 +423,7 @@ impl OverlayBackend for ScreenBackend {
BackendAttrib::StereoAdjustMouse => Some(BackendAttribValue::StereoAdjustMouse(
self.stereo_adjust_mouse,
)),
BackendAttrib::CropRect => Some(BackendAttribValue::CropRect(self.crop_rect)),
_ => None,
}
}
@ -397,6 +457,13 @@ impl OverlayBackend for ScreenBackend {
self.apply_mouse_transform_with_override(frame_transform);
true
}
BackendAttribValue::CropRect(new) => {
self.crop_rect = new;
self.crop_dirty = true;
let frame_transform = mouse_transform_to_transform(self.mouse_transform_override);
self.apply_mouse_transform_with_override(frame_transform);
true
}
_ => false,
}
}

View File

@ -54,18 +54,23 @@ pub struct ScreenPipeline {
pass: SmallVec<[BufPass; 2]>,
pipeline: Arc<WGfxPipeline<Vert2Uv>>,
extentf: [f32; 2],
source_extentf: [f32; 2],
offsetf: [f32; 2],
crop_rect: [f32; 4],
stereo: StereoMode,
}
impl ScreenPipeline {
pub fn new(
meta: &FrameMeta,
source_extent: [u32; 2],
app: &mut AppState,
stereo: StereoMode,
offsetf: [f32; 2],
crop_rect: [f32; 4],
) -> anyhow::Result<Self> {
let extentf = [meta.extent[0] as f32, meta.extent[1] as f32];
let source_extentf = [source_extent[0] as f32, source_extent[1] as f32];
let pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
@ -80,7 +85,9 @@ impl ScreenPipeline {
mouse: Self::create_mouse_pass(app, pipeline.clone(), extentf, offsetf)?,
pipeline,
extentf,
source_extentf,
offsetf,
crop_rect,
stereo,
};
me.ensure_stereo(stereo);
@ -110,8 +117,10 @@ impl ScreenPipeline {
self.pass.pop();
}
let stereo = self.stereo;
let crop_rect = self.crop_rect;
for (eye, current) in self.pass.iter_mut().enumerate() {
let verts = stereo_mode_to_verts(self.stereo, eye);
let verts = cropped_stereo_mode_to_verts(stereo, crop_rect, eye);
current.buf_vert.write()?.copy_from_slice(&verts);
}
Ok(())
@ -122,8 +131,10 @@ impl ScreenPipeline {
app: &mut AppState,
extentf: [f32; 2],
offsetf: [f32; 2],
source_extentf: [f32; 2],
) -> anyhow::Result<()> {
self.extentf = extentf;
self.source_extentf = source_extentf;
self.offsetf = offsetf;
self.pass.clear();
@ -131,6 +142,16 @@ impl ScreenPipeline {
Ok(())
}
pub fn set_crop_rect(&mut self, crop_rect: [f32; 4]) -> anyhow::Result<()> {
self.crop_rect = crop_rect;
let stereo = self.stereo;
for (eye, current) in self.pass.iter_mut().enumerate() {
let verts = cropped_stereo_mode_to_verts(stereo, crop_rect, eye);
current.buf_vert.write()?.copy_from_slice(&verts);
}
Ok(())
}
fn create_pass(
app: &mut AppState,
pipeline: Arc<WGfxPipeline<Vert2Uv>>,
@ -220,15 +241,26 @@ impl ScreenPipeline {
cmd_buf.run_ref(&current.pass)?;
if let Some(mouse) = mouse.as_ref() {
let size = CURSOR_SIZE * self.extentf[1];
let [crop_x, crop_y, crop_w, crop_h] = self.crop_rect;
if mouse.x < crop_x
|| mouse.x > crop_x + crop_w
|| mouse.y < crop_y
|| mouse.y > crop_y + crop_h
{
continue;
}
let local_mouse_x = (mouse.x - crop_x) / crop_w;
let local_mouse_y = (mouse.y - crop_y) / crop_h;
let size = CURSOR_SIZE * self.source_extentf[1] / crop_h.max(0.01);
let half_size = size * 0.5;
upload_quad_vertices(
&mut self.mouse.buf_vert,
self.extentf[0],
self.extentf[1],
mouse.x.mul_add(self.extentf[0], -half_size),
mouse.y.mul_add(self.extentf[1], -half_size),
local_mouse_x.mul_add(self.extentf[0], -half_size),
local_mouse_y.mul_add(self.extentf[1], -half_size),
size,
size,
)?;
@ -241,6 +273,21 @@ impl ScreenPipeline {
}
}
fn cropped_stereo_mode_to_verts(
stereo: StereoMode,
crop_rect: [f32; 4],
array_index: usize,
) -> [Vert2Uv; 4] {
let [crop_x, crop_y, crop_w, crop_h] = crop_rect;
stereo_mode_to_verts(stereo, array_index).map(|vert| Vert2Uv {
in_pos: vert.in_pos,
in_uv: [
crop_x + vert.in_uv[0] * crop_w,
crop_y + vert.in_uv[1] * crop_h,
],
})
}
fn stereo_mode_to_verts(stereo: StereoMode, array_index: usize) -> [Vert2Uv; 4] {
let eye = match stereo {
StereoMode::RightLeft | StereoMode::BottomTop => (1 - array_index) as f32,

View File

@ -352,6 +352,7 @@ impl OverlayBackend for WvrWindowBackend {
app,
[inner_extent[0] as _, inner_extent[1] as _],
[BORDER_SIZE as _, (BAR_SIZE + BORDER_SIZE) as _],
[inner_extent[0] as _, inner_extent[1] as _],
)?;
self.apply_extent(app, &meta)?;
self.inner_extent = inner_extent;
@ -359,9 +360,11 @@ impl OverlayBackend for WvrWindowBackend {
} else {
let pipeline = ScreenPipeline::new(
&meta,
inner_extent,
app,
self.stereo.unwrap_or(StereoMode::None),
[BORDER_SIZE as _, (BAR_SIZE + BORDER_SIZE) as _],
[0.0, 0.0, 1.0, 1.0],
)?;
self.apply_extent(app, &meta)?;
self.pipeline = Some(pipeline);

View File

@ -25,6 +25,7 @@ pub enum BackendAttrib {
StereoFullFrame,
StereoAdjustMouse,
MouseTransform,
CropRect,
Icon,
}
@ -34,6 +35,7 @@ pub enum BackendAttribValue {
StereoFullFrame(bool),
StereoAdjustMouse(bool),
MouseTransform(MouseTransform),
CropRect([f32; 4]),
#[serde(skip_serializing, skip_deserializing)]
Icon(Arc<str>),
}
@ -45,6 +47,7 @@ impl BackendAttribValue {
Self::StereoFullFrame(val) => !*val,
Self::StereoAdjustMouse(val) => !*val,
Self::MouseTransform(val) => *val == MouseTransform::default(),
Self::CropRect(val) => *val == [0.0, 0.0, 1.0, 1.0],
Self::Icon(_) => false,
}
}