Added a new display geometry type and moved all geometry uniform calculations to it.

This commit is contained in:
Filipe Rodrigues 2025-09-16 14:14:42 +01:00
parent ca8b98e106
commit ddf753e466
Signed by: zenithsiz
SSH Key Fingerprint: SHA256:Mb5ppb3Sh7IarBO/sBTXLHbYEOz37hJAlslLQPPAPaU
7 changed files with 225 additions and 192 deletions

View File

@ -1,12 +1,16 @@
//! Display
// Modules
pub mod geometry;
pub mod ser;
// Exports
pub use self::geometry::DisplayGeometry;
// Imports
use {
std::{borrow::Borrow, fmt, sync::Arc},
zsw_util::{Rect, ResourceManager, resource_manager},
zsw_util::{ResourceManager, resource_manager},
};
/// Displays
@ -19,7 +23,7 @@ pub struct Display {
pub name: DisplayName,
/// Geometries
pub geometries: Vec<Rect<i32, u32>>,
pub geometries: Vec<DisplayGeometry>,
}
impl resource_manager::FromSerialized<DisplayName, ser::Display> for Display {
@ -29,7 +33,7 @@ impl resource_manager::FromSerialized<DisplayName, ser::Display> for Display {
geometries: display
.geometries
.into_iter()
.map(|geometry| geometry.geometry)
.map(|geometry| DisplayGeometry::new(geometry.geometry))
.collect(),
}
}
@ -41,7 +45,9 @@ impl resource_manager::ToSerialized<DisplayName, ser::Display> for Display {
geometries: self
.geometries
.iter()
.map(|&geometry| ser::DisplayGeometry { geometry })
.map(|&geometry| ser::DisplayGeometry {
geometry: geometry.into_inner(),
})
.collect(),
}
}

109
zsw/src/display/geometry.rs Normal file
View File

@ -0,0 +1,109 @@
//! Display geometry
// Imports
use {
cgmath::{Matrix4, Vector2, Vector3},
num_rational::Rational32,
winit::dpi::PhysicalSize,
zsw_util::Rect,
};
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct DisplayGeometry {
/// Inner geometry
inner: Rect<i32, u32>,
}
impl DisplayGeometry {
/// Creates a new geometry
pub fn new(inner: Rect<i32, u32>) -> Self {
Self { inner }
}
/// Unwraps this geometry into the inner rect
pub(super) fn into_inner(self) -> Rect<i32, u32> {
self.inner
}
/// Gets the inner rectangle mutably
pub fn as_rect_mut(&mut self) -> &mut Rect<i32, u32> {
&mut self.inner
}
/// Returns if this geometry intersects a window
pub fn intersects_window(&self, window_geometry: Rect<i32, u32>) -> bool {
self.inner.intersection(window_geometry).is_some()
}
/// Returns this geometry's rectangle for a certain window
pub fn on_window(&self, window_geometry: Rect<i32, u32>) -> Rect<i32, u32> {
let mut geometry = self.inner;
geometry.pos -= Vector2::new(window_geometry.pos.x, window_geometry.pos.y);
geometry
}
/// Calculates this panel's position matrix
// Note: This matrix simply goes from a geometry in physical units
// onto shader coordinates.
#[must_use]
pub fn pos_matrix(&self, window_geometry: Rect<i32, u32>, surface_size: PhysicalSize<u32>) -> Matrix4<f32> {
let geometry = self.on_window(window_geometry);
let x_scale = geometry.size[0] as f32 / surface_size.width as f32;
let y_scale = geometry.size[1] as f32 / surface_size.height as f32;
let x_offset = geometry.pos[0] as f32 / surface_size.width as f32;
let y_offset = geometry.pos[1] as f32 / surface_size.height as f32;
let translation = Matrix4::from_translation(Vector3::new(
-1.0 + x_scale + 2.0 * x_offset,
1.0 - y_scale - 2.0 * y_offset,
0.0,
));
let scaling = Matrix4::from_nonuniform_scale(x_scale, -y_scale, 1.0);
translation * scaling
}
/// Calculates an image's ratio for this panel geometry
///
/// This ratio is multiplied by the base uvs to fix the stretching
/// that comes from having a square coordinate system [0.0 .. 1.0] x [0.0 .. 1.0]
pub fn image_ratio(&self, image_size: Vector2<u32>) -> Vector2<f32> {
let image_size = image_size.cast().expect("Image size didn't fit into an `i32`");
let panel_size = self.inner.size.cast().expect("Panel size didn't fit into an `i32`");
// If either the image or our panel have a side with 0, return a square ratio
// TODO: Check if this is the right thing to do
if panel_size.x == 0 || panel_size.y == 0 || image_size.x == 0 || image_size.y == 0 {
return Vector2::new(0.0, 0.0);
}
// Image and panel ratios
let image_ratio = Rational32::new(image_size.x, image_size.y);
let panel_ratio = Rational32::new(panel_size.x, panel_size.y);
// Ratios between the image and panel
let width_ratio = Rational32::new(panel_size.x, image_size.x);
let height_ratio = Rational32::new(panel_size.y, image_size.y);
// X-axis ratio, if image scrolls horizontally
let x_ratio = self::ratio_as_f32(width_ratio / height_ratio);
// Y-axis ratio, if image scrolls vertically
let y_ratio = self::ratio_as_f32(height_ratio / width_ratio);
match image_ratio >= panel_ratio {
true => Vector2::new(x_ratio, 1.0),
false => Vector2::new(1.0, y_ratio),
}
}
}
/// Converts a `Ratio<i32>` to `f32`, rounding
// TODO: Although image and window sizes fit into an `f32`, maybe a
// rational of the two wouldn't fit properly when in a num / denom
// format, since both may be bigger than `2^24`, check if this is fine.
fn ratio_as_f32(ratio: Rational32) -> f32 {
*ratio.numer() as f32 / *ratio.denom() as f32
}

View File

@ -46,7 +46,7 @@ use {
display::Displays,
menu::Menu,
metrics::Metrics,
panel::{PanelGeometry, Panels, PanelsRenderer, PanelsRendererShared},
panel::{Panels, PanelsRenderer, PanelsRendererShared},
playlist::Playlists,
profile::{ProfileName, Profiles},
shared::Shared,
@ -376,96 +376,97 @@ async fn paint_egui(
menu: &mut Menu,
window_geometry: Rect<i32, u32>,
) -> Result<(Vec<egui::ClippedPrimitive>, egui::TexturesDelta), AppError> {
let full_output_fut =
egui_painter.draw(window, async |ctx| {
// Draw the menu
tokio::task::block_in_place(|| {
menu.draw(
ctx,
&shared.wgpu,
&shared.displays,
&shared.playlists,
&shared.profiles,
&shared.panels,
&shared.metrics,
&shared.window_monitor_names,
&shared.event_loop_proxy,
window_geometry,
)
});
// Then go through all panels checking for interactions with their geometries
// TODO: Should this be done here and not somewhere else?
let Some(pointer_pos) = ctx.input(|input| input.pointer.latest_pos()) else {
return Ok(());
};
let pointer_pos = Point2::new(pointer_pos.x as i32, pointer_pos.y as i32);
shared
.panels
.for_each(async |panel| {
let panel = &mut *panel.lock().await;
let display = panel.display.read().await;
// If we're over an egui area, or none of the geometries are underneath the cursor, skip the panel
if ctx.is_pointer_over_area() ||
!display.geometries.iter().any(|&geometry| {
PanelGeometry::geometry_on(geometry, window_geometry).contains(pointer_pos)
}) {
return;
}
// Pause any double-clicked panels
if ctx.input(|input| input.pointer.button_double_clicked(egui::PointerButton::Primary)) {
#[expect(clippy::match_same_arms, reason = "We'll be changing them soon")]
match &mut panel.state {
panel::PanelState::None(_) => (),
panel::PanelState::Fade(state) => state.toggle_paused(),
panel::PanelState::Slide(_) => (),
}
}
// Skip any ctrl-clicked/middle clicked panels
if ctx.input(|input| {
(input.pointer.button_clicked(egui::PointerButton::Primary) && input.modifiers.ctrl) ||
input.pointer.button_clicked(egui::PointerButton::Middle)
}) {
#[expect(clippy::match_same_arms, reason = "We'll be changing them soon")]
match &mut panel.state {
panel::PanelState::None(_) => (),
panel::PanelState::Fade(state) => state.skip(&shared.wgpu).await,
panel::PanelState::Slide(_) => (),
}
}
// Scroll panels
let scroll_delta = ctx.input(|input| input.smooth_scroll_delta.y);
if scroll_delta != 0.0 {
#[expect(clippy::match_same_arms, reason = "We'll be changing them soon")]
match &mut panel.state {
panel::PanelState::None(_) => (),
panel::PanelState::Fade(state) => {
// TODO: Make this "speed" configurable
// TODO: Perform the conversion better without going through nanos
let speed = 1.0 / 1000.0;
let time_delta_abs = state.duration().mul_f32(scroll_delta.abs() * speed);
let time_delta_abs =
TimeDelta::from_std(time_delta_abs).expect("Offset didn't fit into time delta");
let time_delta = match scroll_delta.is_sign_positive() {
true => -time_delta_abs,
false => time_delta_abs,
};
state.step(&shared.wgpu, time_delta).await;
},
panel::PanelState::Slide(_) => (),
}
}
})
.await;
Ok::<_, !>(())
let full_output_fut = egui_painter.draw(window, async |ctx| {
// Draw the menu
tokio::task::block_in_place(|| {
menu.draw(
ctx,
&shared.wgpu,
&shared.displays,
&shared.playlists,
&shared.profiles,
&shared.panels,
&shared.metrics,
&shared.window_monitor_names,
&shared.event_loop_proxy,
window_geometry,
)
});
// Then go through all panels checking for interactions with their geometries
// TODO: Should this be done here and not somewhere else?
let Some(pointer_pos) = ctx.input(|input| input.pointer.latest_pos()) else {
return Ok(());
};
let pointer_pos = Point2::new(pointer_pos.x as i32, pointer_pos.y as i32);
shared
.panels
.for_each(async |panel| {
let panel = &mut *panel.lock().await;
let display = panel.display.read().await;
// If we're over an egui area, or none of the geometries are underneath the cursor, skip the panel
if ctx.is_pointer_over_area() ||
!display
.geometries
.iter()
.any(|&geometry| geometry.on_window(window_geometry).contains(pointer_pos))
{
return;
}
// Pause any double-clicked panels
if ctx.input(|input| input.pointer.button_double_clicked(egui::PointerButton::Primary)) {
#[expect(clippy::match_same_arms, reason = "We'll be changing them soon")]
match &mut panel.state {
panel::PanelState::None(_) => (),
panel::PanelState::Fade(state) => state.toggle_paused(),
panel::PanelState::Slide(_) => (),
}
}
// Skip any ctrl-clicked/middle clicked panels
if ctx.input(|input| {
(input.pointer.button_clicked(egui::PointerButton::Primary) && input.modifiers.ctrl) ||
input.pointer.button_clicked(egui::PointerButton::Middle)
}) {
#[expect(clippy::match_same_arms, reason = "We'll be changing them soon")]
match &mut panel.state {
panel::PanelState::None(_) => (),
panel::PanelState::Fade(state) => state.skip(&shared.wgpu).await,
panel::PanelState::Slide(_) => (),
}
}
// Scroll panels
let scroll_delta = ctx.input(|input| input.smooth_scroll_delta.y);
if scroll_delta != 0.0 {
#[expect(clippy::match_same_arms, reason = "We'll be changing them soon")]
match &mut panel.state {
panel::PanelState::None(_) => (),
panel::PanelState::Fade(state) => {
// TODO: Make this "speed" configurable
// TODO: Perform the conversion better without going through nanos
let speed = 1.0 / 1000.0;
let time_delta_abs = state.duration().mul_f32(scroll_delta.abs() * speed);
let time_delta_abs =
TimeDelta::from_std(time_delta_abs).expect("Offset didn't fit into time delta");
let time_delta = match scroll_delta.is_sign_positive() {
true => -time_delta_abs,
false => time_delta_abs,
};
state.step(&shared.wgpu, time_delta).await;
},
panel::PanelState::Slide(_) => (),
}
}
})
.await;
Ok::<_, !>(())
});
let full_output = full_output_fut.await?;
let paint_jobs = egui_painter
.tessellate_shapes(full_output.shapes, full_output.pixels_per_point)

View File

@ -2,7 +2,7 @@
// Imports
use {
crate::display::{Display, DisplayName, Displays},
crate::display::{Display, DisplayGeometry, DisplayName, Displays},
std::sync::Arc,
zsw_util::{Rect, TokioTaskBlockOn},
zutil_cloned::cloned,
@ -28,7 +28,7 @@ pub fn draw_displays_tab(ui: &mut egui::Ui, displays: &Arc<Displays>) {
ui.collapsing("New", |ui| {
let name = super::get_data::<String>(ui, "display-tab-new-name");
let geometries = super::get_data::<Vec<Rect<i32, u32>>>(ui, "display-tab-new-geometries");
let geometries = super::get_data::<Vec<DisplayGeometry>>(ui, "display-tab-new-geometries");
ui.horizontal(|ui| {
ui.label("Name");
@ -58,13 +58,13 @@ pub fn draw_displays_tab(ui: &mut egui::Ui, displays: &Arc<Displays>) {
/// Draws a display's geometries
pub fn draw_display_geometries(ui: &mut egui::Ui, geometries: &mut Vec<Rect<i32, u32>>) {
pub fn draw_display_geometries(ui: &mut egui::Ui, geometries: &mut Vec<DisplayGeometry>) {
let mut geometry_idx = 0;
geometries.retain_mut(|geometry| {
let mut retain = true;
ui.horizontal(|ui| {
ui.label(format!("#{}: ", geometry_idx + 1));
super::draw_rect(ui, geometry);
super::draw_rect(ui, geometry.as_rect_mut());
if ui.button("-").clicked() {
retain = false;
}
@ -75,6 +75,6 @@ pub fn draw_display_geometries(ui: &mut egui::Ui, geometries: &mut Vec<Rect<i32,
});
if ui.button("+").clicked() {
geometries.push(Rect::zero());
geometries.push(DisplayGeometry::new(Rect::zero()));
}
}

View File

@ -42,7 +42,7 @@ fn draw_panels_editor(ui: &mut egui::Ui, wgpu: &Wgpu, panels: &Panels, window_ge
if display
.geometries
.iter()
.all(|&geometry| window_geometry.intersection(geometry).is_none())
.all(|&geometry| !geometry.intersects_window(window_geometry))
{
name = name.weak();
}
@ -83,12 +83,12 @@ fn draw_fade_panel_editor(
for (geometry_idx, geometry) in display.geometries.iter_mut().enumerate() {
ui.horizontal(|ui| {
let mut name = egui::WidgetText::from(format!("#{}: ", geometry_idx + 1));
if window_geometry.intersection(*geometry).is_none() {
if !geometry.intersects_window(window_geometry) {
name = name.weak();
}
ui.label(name);
super::draw_rect(ui, geometry);
super::draw_rect(ui, geometry.as_rect_mut());
});
}
});

View File

@ -1,13 +1,7 @@
//! Panel geometry
// Imports
use {
cgmath::{Matrix4, Vector2, Vector3},
num_rational::Rational32,
std::collections::HashMap,
winit::{dpi::PhysicalSize, window::WindowId},
zsw_util::Rect,
};
use {std::collections::HashMap, winit::window::WindowId};
/// Panel geometry
@ -33,82 +27,4 @@ impl PanelGeometry {
uniforms: HashMap::new(),
}
}
// TODO: Move these out of here, since they don't receive `&self`.
/// Returns this geometry's rectangle for a certain window
pub fn geometry_on(display_geometry: Rect<i32, u32>, window_geometry: Rect<i32, u32>) -> Rect<i32, u32> {
let mut geometry = display_geometry;
geometry.pos -= Vector2::new(window_geometry.pos.x, window_geometry.pos.y);
geometry
}
/// Calculates this panel's position matrix
// Note: This matrix simply goes from a geometry in physical units
// onto shader coordinates.
#[must_use]
pub fn pos_matrix(
display_geometry: Rect<i32, u32>,
window_geometry: Rect<i32, u32>,
surface_size: PhysicalSize<u32>,
) -> Matrix4<f32> {
let geometry = Self::geometry_on(display_geometry, window_geometry);
let x_scale = geometry.size[0] as f32 / surface_size.width as f32;
let y_scale = geometry.size[1] as f32 / surface_size.height as f32;
let x_offset = geometry.pos[0] as f32 / surface_size.width as f32;
let y_offset = geometry.pos[1] as f32 / surface_size.height as f32;
let translation = Matrix4::from_translation(Vector3::new(
-1.0 + x_scale + 2.0 * x_offset,
1.0 - y_scale - 2.0 * y_offset,
0.0,
));
let scaling = Matrix4::from_nonuniform_scale(x_scale, -y_scale, 1.0);
translation * scaling
}
/// Calculates an image's ratio for this panel geometry
///
/// This ratio is multiplied by the base uvs to fix the stretching
/// that comes from having a square coordinate system [0.0 .. 1.0] x [0.0 .. 1.0]
pub fn image_ratio(panel_size: Vector2<u32>, image_size: Vector2<u32>) -> Vector2<f32> {
let image_size = image_size.cast().expect("Image size didn't fit into an `i32`");
let panel_size = panel_size.cast().expect("Panel size didn't fit into an `i32`");
// If either the image or our panel have a side with 0, return a square ratio
// TODO: Check if this is the right thing to do
if panel_size.x == 0 || panel_size.y == 0 || image_size.x == 0 || image_size.y == 0 {
return Vector2::new(0.0, 0.0);
}
// Image and panel ratios
let image_ratio = Rational32::new(image_size.x, image_size.y);
let panel_ratio = Rational32::new(panel_size.x, panel_size.y);
// Ratios between the image and panel
let width_ratio = Rational32::new(panel_size.x, image_size.x);
let height_ratio = Rational32::new(panel_size.y, image_size.y);
// X-axis ratio, if image scrolls horizontally
let x_ratio = self::ratio_as_f32(width_ratio / height_ratio);
// Y-axis ratio, if image scrolls vertically
let y_ratio = self::ratio_as_f32(height_ratio / width_ratio);
match image_ratio >= panel_ratio {
true => Vector2::new(x_ratio, 1.0),
false => Vector2::new(1.0, y_ratio),
}
}
}
/// Converts a `Ratio<i32>` to `f32`, rounding
// TODO: Although image and window sizes fit into an `f32`, maybe a
// rational of the two wouldn't fit properly when in a num / denom
// format, since both may be bigger than `2^24`, check if this is fine.
fn ratio_as_f32(ratio: Rational32) -> f32 {
*ratio.numer() as f32 / *ratio.denom() as f32
}

View File

@ -12,6 +12,7 @@ use {
self::uniform::PanelImageUniforms,
super::{PanelGeometryUniforms, PanelState, Panels, state::fade::PanelFadeImagesShared},
crate::{
display::DisplayGeometry,
metrics::{self, Metrics},
panel::{PanelGeometry, state::fade::PanelFadeImage},
time,
@ -252,11 +253,11 @@ impl PanelsRenderer {
create_render_pipeline,
geometries: HashMap::new(),
});
for (geometry_idx, (&display_geometry, panel_geometry)) in
for (geometry_idx, (display_geometry, panel_geometry)) in
display.geometries.iter().zip_eq(&mut panel.geometries).enumerate()
{
// If this geometry is outside our window, we can safely ignore it
if window_geometry.intersection(display_geometry).is_none() {
if !display_geometry.intersects_window(window_geometry) {
continue;
}
@ -307,12 +308,12 @@ impl PanelsRenderer {
panel_state: &PanelState,
window_geometry: Rect<i32, u32>,
window: &Window,
display_geometry: Rect<i32, u32>,
display_geometry: &DisplayGeometry,
panel_geometry: &mut PanelGeometry,
render_pass: &mut wgpu::RenderPass<'_>,
) {
// Calculate the position matrix for the panel
let pos_matrix = PanelGeometry::pos_matrix(display_geometry, window_geometry, surface_size);
let pos_matrix = display_geometry.pos_matrix(window_geometry, surface_size);
let pos_matrix = uniform::Matrix4x4(pos_matrix.into());
// Writes uniforms `uniforms`
@ -343,7 +344,7 @@ impl PanelsRenderer {
},
};
let ratio = PanelGeometry::image_ratio(display_geometry.size, size);
let ratio = display_geometry.image_ratio(size);
PanelImageUniforms::new(ratio, swap_dir)
};