From 278bf6bfe596ce1dc786f98ae780203739f95210 Mon Sep 17 00:00:00 2001 From: Kaylee Simmons Date: Fri, 19 Apr 2024 13:34:31 -0700 Subject: [PATCH] feat: render same z-index together (#2467) * feat: render same z-index together * make clippy happy * uniform blends in layer, render all background first, then foreground * fix background blends * add test for telescope cases * fix telescope case * fix test case * Remove custom hull algorithm in favor of skia's built in one * Remove zindex step setting and use consecutive groupings of floating windows instead * Swap to iter_lines * Add back in the floating order which was removed due to a clippy warning but is now needed again * Address clippy issues and refactor rendered window lines functions to split out their components into parts * Fix overzealous clipping --------- Co-authored-by: Hawtian Wang Co-authored-by: Hawtian Wang --- .gitignore | 5 + Cargo.lock | 7 + Cargo.toml | 1 + src/renderer/mod.rs | 79 ++++++- src/renderer/rendered_layer.rs | 224 ++++++++++++++++++++ src/renderer/rendered_window.rs | 362 +++++++++++++++++++------------- src/units.rs | 10 + src/utils/mod.rs | 4 + src/utils/test.rs | 218 +++++++++++++++++++ 9 files changed, 752 insertions(+), 158 deletions(-) create mode 100644 src/renderer/rendered_layer.rs create mode 100644 src/utils/test.rs diff --git a/.gitignore b/.gitignore index 8757aca2..a78e5196 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ .DS_Store website/book *.AppImage +.envrc +flake.nix +flake.lock +.direnv/ +.devenv/ diff --git a/Cargo.lock b/Cargo.lock index a375c4e5..13c89ec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1147,6 +1147,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "inotify" version = "0.9.6" @@ -1447,6 +1453,7 @@ dependencies = [ "glutin-winit", "icrate", "image", + "indoc", "itertools", "lazy_static", "log", diff --git a/Cargo.toml b/Cargo.toml index a3194adf..114591bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ gl = "0.14.0" glutin = "0.31.1" glutin-winit = "0.4.2" image = { version = "0.25.0", default-features = false, features = ["ico"] } +indoc = "2.0.5" itertools = "0.12.1" lazy_static = "1.4.0" log = "0.4.16" diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 8d3d96e3..06630e49 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -4,6 +4,7 @@ pub mod fonts; pub mod grid_renderer; pub mod opengl; pub mod profiler; +mod rendered_layer; mod rendered_window; mod vsync; @@ -16,6 +17,7 @@ use std::{ sync::Arc, }; +use itertools::Itertools; use log::{error, warn}; use skia_safe::Canvas; use winit::{ @@ -28,6 +30,7 @@ use crate::{ bridge::EditorMode, editor::{Cursor, Style}, profiling::{tracy_create_gpu_context, tracy_named_frame, tracy_zone}, + renderer::rendered_layer::{group_windows, FloatingLayer}, settings::*, units::{to_skia_rect, GridPos, GridRect, GridSize, PixelPos}, window::{ShouldRender, UserEvent}, @@ -180,7 +183,7 @@ impl Renderer { root_canvas.clip_rect(clip_rect, None, Some(false)); } - let windows: Vec<&mut RenderedWindow> = { + let (root_windows, floating_layers) = { let (mut root_windows, mut floating_windows): ( Vec<&mut RenderedWindow>, Vec<&mut RenderedWindow>, @@ -192,16 +195,61 @@ impl Renderer { root_windows .sort_by(|window_a, window_b| window_a.id.partial_cmp(&window_b.id).unwrap()); - floating_windows.sort_by(floating_sort); - root_windows.into_iter().chain(floating_windows).collect() + let mut floating_layers = vec![]; + + let mut base_zindex = 0; + let mut last_zindex = 0; + let mut current_windows = vec![]; + + for window in floating_windows { + let zindex = window.anchor_info.as_ref().unwrap().sort_order; + log::debug!("zindex: {}, base: {}", zindex, base_zindex); + // Group floating windows by consecutive z indices + if zindex - last_zindex > 1 && !current_windows.is_empty() { + for windows in group_windows(current_windows, grid_scale) { + floating_layers.push(FloatingLayer { + sort_order: base_zindex, + windows, + }); + } + current_windows = vec![]; + } + + if current_windows.is_empty() { + base_zindex = zindex; + } + current_windows.push(window); + last_zindex = zindex; + } + + if !current_windows.is_empty() { + for windows in group_windows(current_windows, grid_scale) { + floating_layers.push(FloatingLayer { + sort_order: base_zindex, + windows, + }); + } + } + + for layer in &mut floating_layers { + layer.windows.sort_by(floating_sort); + log::debug!( + "layer: {:?}", + layer + .windows + .iter() + .map(|w| (w.id, w.anchor_info.as_ref().unwrap().sort_order)) + .collect_vec() + ); + } + + (root_windows, floating_layers) }; let settings = SETTINGS.get::(); - let mut floating_rects = Vec::new(); - - self.window_regions = windows + let root_window_regions = root_windows .into_iter() .map(|window| { window.draw( @@ -209,11 +257,26 @@ impl Renderer { &settings, default_background.with_a((255.0 * transparency) as u8), grid_scale, - &mut floating_rects, ) }) - .collect(); + .collect_vec(); + let floating_window_regions = floating_layers + .into_iter() + .flat_map(|mut layer| { + layer.draw( + root_canvas, + &settings, + default_background.with_a((255.0 * transparency) as u8), + grid_scale, + ) + }) + .collect_vec(); + + self.window_regions = root_window_regions + .into_iter() + .chain(floating_window_regions) + .collect(); self.cursor_renderer .draw(&mut self.grid_renderer, root_canvas); diff --git a/src/renderer/rendered_layer.rs b/src/renderer/rendered_layer.rs new file mode 100644 index 00000000..8bfa8a83 --- /dev/null +++ b/src/renderer/rendered_layer.rs @@ -0,0 +1,224 @@ +use itertools::Itertools; +use skia_safe::{ + canvas::SaveLayerRec, + image_filters::blur, + utils::shadow_utils::{draw_shadow, ShadowFlags}, + BlendMode, Canvas, ClipOp, Color, Paint, Path, PathOp, Point3, Rect, +}; + +use crate::units::{to_skia_rect, GridScale, PixelRect}; + +use super::{RenderedWindow, RendererSettings, WindowDrawDetails}; + +struct LayerWindow<'w> { + window: &'w mut RenderedWindow, + group: usize, +} + +pub struct FloatingLayer<'w> { + pub sort_order: u64, + pub windows: Vec<&'w mut RenderedWindow>, +} + +impl<'w> FloatingLayer<'w> { + pub fn draw( + &mut self, + root_canvas: &Canvas, + settings: &RendererSettings, + default_background: Color, + grid_scale: GridScale, + ) -> Vec { + let pixel_regions = self + .windows + .iter() + .map(|window| window.pixel_region(grid_scale)) + .collect::>(); + let (silhouette, bound_rect) = build_silhouette(&pixel_regions); + let has_transparency = default_background.a() != 255 + || self.windows.iter().any(|window| window.has_transparency()); + + self._draw_shadow(root_canvas, &silhouette, settings); + + root_canvas.save(); + root_canvas.clip_path(&silhouette, None, Some(false)); + let need_blur = has_transparency || settings.floating_blur; + + if need_blur { + if let Some(blur) = blur( + ( + settings.floating_blur_amount_x, + settings.floating_blur_amount_y, + ), + None, + None, + None, + ) { + let paint = Paint::default() + .set_anti_alias(false) + .set_blend_mode(BlendMode::Src) + .to_owned(); + let save_layer_rec = SaveLayerRec::default() + .backdrop(&blur) + .bounds(&bound_rect) + .paint(&paint); + root_canvas.save_layer(&save_layer_rec); + root_canvas.restore(); + } + } + + let paint = Paint::default() + .set_anti_alias(false) + .set_color(Color::from_argb(255, 255, 255, default_background.a())) + .set_blend_mode(BlendMode::SrcOver) + .to_owned(); + + let save_layer_rec = SaveLayerRec::default().bounds(&bound_rect).paint(&paint); + + root_canvas.save_layer(&save_layer_rec); + root_canvas.clear(default_background.with_a(255)); + + let regions = self + .windows + .iter() + .map(|window| window.pixel_region(grid_scale)) + .collect::>(); + + let blend = self.uniform_background_blend(); + + self.windows.iter_mut().for_each(|window| { + window.update_blend(blend); + }); + + let mut ret = vec![]; + + (0..self.windows.len()).for_each(|i| { + let window = &mut self.windows[i]; + window.draw_background_surface(root_canvas, regions[i], grid_scale); + }); + (0..self.windows.len()).for_each(|i| { + let window = &mut self.windows[i]; + window.draw_foreground_surface(root_canvas, regions[i], grid_scale); + + ret.push(WindowDrawDetails { + id: window.id, + region: regions[i], + floating_order: window.anchor_info.as_ref().map(|v| v.sort_order), + }); + }); + + root_canvas.restore(); + + root_canvas.restore(); + + ret + } + + pub fn uniform_background_blend(&self) -> u8 { + self.windows + .iter() + .filter_map(|window| window.get_smallest_blend_value()) + .min() + .unwrap_or(0) + } + + fn _draw_shadow(&self, root_canvas: &Canvas, path: &Path, settings: &RendererSettings) { + root_canvas.save(); + // We clip using the Difference op to make sure that the shadow isn't rendered inside + // the window itself. + root_canvas.clip_path(path, Some(ClipOp::Difference), None); + // The light angle is specified in degrees from the vertical, so we first convert them + // to radians and then use sin/cos to get the y and z components of the light + let light_angle_radians = settings.light_angle_degrees.to_radians(); + draw_shadow( + root_canvas, + path, + // Specifies how far from the root canvas the shadow casting rect is. We just use + // the z component here to set it a constant distance away. + Point3::new(0., 0., settings.floating_z_height), + // Because we use the DIRECTIONAL_LIGHT shadow flag, this specifies the angle that + // the light is coming from. + Point3::new(0., -light_angle_radians.sin(), light_angle_radians.cos()), + // This is roughly equal to the apparent radius of the light . + 5., + Color::from_argb((0.03 * 255.) as u8, 0, 0, 0), + Color::from_argb((0.35 * 255.) as u8, 0, 0, 0), + // Directional Light flag is necessary to make the shadow render consistently + // across various sizes of floating windows. It effects how the light direction is + // processed. + Some(ShadowFlags::DIRECTIONAL_LIGHT), + ); + root_canvas.restore(); + } +} + +fn get_window_group(windows: &mut Vec, index: usize) -> usize { + if windows[index].group != index { + windows[index].group = get_window_group(windows, windows[index].group); + } + windows[index].group +} + +fn rect_intersect(a: &Rect, b: &Rect) -> bool { + Rect::intersects2(a, b) +} + +fn group_windows_with_regions(windows: &mut Vec, regions: &[PixelRect]) { + for i in 0..windows.len() { + for j in i + 1..windows.len() { + let group_i = get_window_group(windows, i); + let group_j = get_window_group(windows, j); + if group_i != group_j + && rect_intersect(&to_skia_rect(®ions[i]), &to_skia_rect(®ions[j])) + { + let new_group = group_i.min(group_j); + if group_i != group_j { + windows[group_i].group = new_group; + windows[group_j].group = new_group; + } + } + } + } +} + +pub fn group_windows( + windows: Vec<&mut RenderedWindow>, + grid_scale: GridScale, +) -> Vec> { + let mut windows = windows + .into_iter() + .enumerate() + .map(|(index, window)| LayerWindow { + window, + group: index, + }) + .collect::>(); + let regions = windows + .iter() + .map(|window| window.window.pixel_region(grid_scale)) + .collect::>(); + group_windows_with_regions(&mut windows, ®ions); + for i in 0..windows.len() { + let _ = get_window_group(&mut windows, i); + } + windows + .into_iter() + .group_by(|window| window.group) + .into_iter() + .map(|(_, v)| v.map(|w| w.window).collect::>()) + .collect_vec() +} + +fn build_silhouette(regions: &[PixelRect]) -> (Path, Rect) { + let silhouette = regions + .iter() + .map(|r| Path::rect(to_skia_rect(r), None)) + .reduce(|a, b| a.op(&b, PathOp::Union).unwrap()) + .unwrap(); + let bounding_rect = regions + .iter() + .map(to_skia_rect) + .reduce(Rect::join2) + .unwrap(); + + (silhouette, bounding_rect) +} diff --git a/src/renderer/rendered_window.rs b/src/renderer/rendered_window.rs index 412c9bf5..5b362845 100644 --- a/src/renderer/rendered_window.rs +++ b/src/renderer/rendered_window.rs @@ -2,7 +2,6 @@ use std::{cell::RefCell, rc::Rc, sync::Arc}; use skia_safe::{ canvas::SaveLayerRec, - image_filters::blur, utils::shadow_utils::{draw_shadow, ShadowFlags}, BlendMode, Canvas, ClipOp, Color, Matrix, Paint, Path, Picture, PictureRecorder, Point3, Rect, }; @@ -13,7 +12,7 @@ use crate::{ profiling::{tracy_plot, tracy_zone}, renderer::{animation_utils::*, GridRenderer, RendererSettings}, settings::SETTINGS, - units::{to_skia_rect, GridPos, GridRect, GridScale, GridSize, PixelRect}, + units::{to_skia_rect, GridPos, GridRect, GridScale, GridSize, PixelRect, PixelVec}, utils::RingBuffer, }; @@ -72,8 +71,8 @@ struct Line { line_fragments: Vec, background_picture: Option, foreground_picture: Option, - has_transparency: bool, is_inferred_border: bool, + blend: u8, is_valid: bool, } @@ -104,6 +103,7 @@ pub struct RenderedWindow { pub struct WindowDrawDetails { pub id: u64, pub region: PixelRect, + pub floating_order: Option, } impl WindowDrawDetails { @@ -116,6 +116,19 @@ impl WindowDrawDetails { } } +impl Line { + fn update_background_blend(&mut self, blend: u8) { + if self.blend != blend { + self.blend = blend; + self.is_valid = false; + } + } + + fn has_transparency(&self) -> bool { + self.blend > 0 + } +} + impl RenderedWindow { pub fn new(id: u64, grid_position: GridPos, grid_size: GridSize) -> RenderedWindow { RenderedWindow { @@ -151,6 +164,13 @@ impl RenderedWindow { * grid_scale } + pub fn update_blend(&self, blend: u8) { + for (_, line) in self.iter_lines() { + let mut line = line.borrow_mut(); + line.update_background_blend(blend); + } + } + fn get_target_position(&self, grid_rect: &GridRect) -> GridPos { let destination = self.grid_destination + grid_rect.min.to_vector(); @@ -221,126 +241,72 @@ impl RenderedWindow { animating } - pub fn draw_surface( + pub fn draw_background_surface( &mut self, canvas: &Canvas, - pixel_region: &Rect, + pixel_region: PixelRect, grid_scale: GridScale, - default_background: Color, ) { - let scroll_offset_lines = self.scroll_animation.position.floor(); - let scroll_offset = scroll_offset_lines - self.scroll_animation.position; - let scroll_offset_lines = scroll_offset_lines as isize; - let scroll_offset_pixels = (scroll_offset * grid_scale.0.height).round() as isize; - let line_height = grid_scale.0.height; let mut has_transparency = false; - let lines: Vec<(Matrix, &Rc>)> = if !self.scrollback_lines.is_empty() { - (0..self.grid_size.height as isize + 1) - .filter_map(|i| { - self.scrollback_lines[scroll_offset_lines + i] - .as_ref() - .map(|line| (i, line)) - }) - .map(|(i, line)| { - let mut matrix = Matrix::new_identity(); - matrix.set_translate(( - pixel_region.left(), - pixel_region.top() - + (scroll_offset_pixels - + ((i + self.viewport_margins.top as isize) - * grid_scale.0.height as isize)) - as f32, - )); - (matrix, line) - }) - .collect() - } else { - Vec::new() - }; + let inner_region = self.inner_region(pixel_region, grid_scale); - let top_border_indices = 0..self.viewport_margins.top as isize; - let actual_line_count = self.actual_lines.len() as isize; - let bottom_border_indices = - actual_line_count - self.viewport_margins.bottom as isize..actual_line_count; - let margins_inferred = self.viewport_margins.inferred; - - let border_lines: Vec<_> = top_border_indices - .chain(bottom_border_indices) - .filter_map(|i| { - self.actual_lines[i].as_ref().and_then(|line| { - if !margins_inferred || line.borrow().is_inferred_border { - Some((i, line)) - } else { - None - } - }) - }) - .map(|(i, line)| { - let mut matrix = Matrix::new_identity(); - matrix.set_translate(( - pixel_region.left(), - pixel_region.top() + (i * grid_scale.0.height as isize) as f32, - )); - (matrix, line) - }) - .collect(); - - let inner_region = Rect::from_xywh( - pixel_region.x(), - pixel_region.y() + self.viewport_margins.top as f32 * line_height, - pixel_region.width(), - pixel_region.height() - - (self.viewport_margins.top + self.viewport_margins.bottom) as f32 * line_height, + canvas.save(); + canvas.clip_rect(to_skia_rect(&pixel_region), None, false); + for (matrix, line) in self.iter_border_lines_with_transform(pixel_region, grid_scale) { + let line = line.borrow(); + if let Some(background_picture) = &line.background_picture { + has_transparency |= line.has_transparency(); + canvas.draw_picture(background_picture, Some(&matrix), None); + } + } + canvas.save(); + canvas.clip_rect(inner_region, None, false); + let mut pics = 0; + for (matrix, line) in self.iter_scrollable_lines_with_transform(pixel_region, grid_scale) { + let line = line.borrow(); + if let Some(background_picture) = &line.background_picture { + has_transparency |= line.has_transparency(); + canvas.draw_picture(background_picture, Some(&matrix), None); + pics += 1; + } + } + log::trace!( + "region: {:?}, inner: {:?}, pics: {}", + pixel_region, + inner_region, + pics ); - - let mut background_paint = Paint::default(); - background_paint.set_blend_mode(BlendMode::Src); - background_paint.set_alpha(default_background.a()); - - let save_layer_rec = SaveLayerRec::default() - .bounds(pixel_region) - .paint(&background_paint); - canvas.save_layer(&save_layer_rec); - canvas.clear(default_background.with_a(255)); - for (matrix, line) in &border_lines { - let line = line.borrow(); - if let Some(background_picture) = &line.background_picture { - has_transparency |= line.has_transparency; - canvas.draw_picture(background_picture, Some(matrix), None); - } - } - canvas.save(); - canvas.clip_rect(inner_region, None, false); - for (matrix, line) in &lines { - let line = line.borrow(); - if let Some(background_picture) = &line.background_picture { - has_transparency |= line.has_transparency; - canvas.draw_picture(background_picture, Some(matrix), None); - } - } canvas.restore(); canvas.restore(); - for (matrix, line) in &border_lines { - let line = line.borrow(); - if let Some(foreground_picture) = &line.foreground_picture { - canvas.draw_picture(foreground_picture, Some(matrix), None); - } - } - canvas.save(); - canvas.clip_rect(inner_region, None, false); - for (matrix, line) in &lines { - let line = line.borrow(); - if let Some(foreground_picture) = &line.foreground_picture { - canvas.draw_picture(foreground_picture, Some(matrix), None); - } - } - canvas.restore(); self.has_transparency = has_transparency; } - fn has_transparency(&self) -> bool { + pub fn draw_foreground_surface( + &mut self, + canvas: &Canvas, + pixel_region: PixelRect, + grid_scale: GridScale, + ) { + for (matrix, line) in self.iter_border_lines_with_transform(pixel_region, grid_scale) { + let line = line.borrow(); + if let Some(foreground_picture) = &line.foreground_picture { + canvas.draw_picture(foreground_picture, Some(&matrix), None); + } + } + canvas.save(); + canvas.clip_rect(self.inner_region(pixel_region, grid_scale), None, false); + for (matrix, line) in self.iter_scrollable_lines_with_transform(pixel_region, grid_scale) { + let line = line.borrow(); + if let Some(foreground_picture) = &line.foreground_picture { + canvas.draw_picture(foreground_picture, Some(&matrix), None); + } + } + canvas.restore(); + } + + pub fn has_transparency(&self) -> bool { let scroll_offset_lines = self.scroll_animation.position.floor() as isize; if self.scrollback_lines.is_empty() { return false; @@ -350,7 +316,7 @@ impl RenderedWindow { scroll_offset_lines..scroll_offset_lines + self.grid_size.height as isize + 1, ) .flatten() - .any(|line| line.borrow().has_transparency) + .any(|line| line.borrow().has_transparency()) } pub fn draw( @@ -359,20 +325,11 @@ impl RenderedWindow { settings: &RendererSettings, default_background: Color, grid_scale: GridScale, - previous_floating_rects: &mut Vec>, ) -> WindowDrawDetails { - let has_transparency = default_background.a() != 255 || self.has_transparency(); - let pixel_region_box = self.pixel_region(grid_scale); let pixel_region = to_skia_rect(&pixel_region_box); - let transparent_floating = self.anchor_info.is_some() && has_transparency; - if self.anchor_info.is_some() - && settings.floating_shadow - && !previous_floating_rects - .iter() - .any(|rect| rect.contains_box(&pixel_region_box)) - { + if self.anchor_info.is_some() && settings.floating_shadow { root_canvas.save(); let shadow_path = Path::rect(pixel_region, None); // We clip using the Difference op to make sure that the shadow isn't rendered inside @@ -400,35 +357,10 @@ impl RenderedWindow { Some(ShadowFlags::DIRECTIONAL_LIGHT), ); root_canvas.restore(); - previous_floating_rects.push(pixel_region_box); } root_canvas.save(); root_canvas.clip_rect(pixel_region, None, Some(false)); - let need_blur = transparent_floating && settings.floating_blur; - - if need_blur { - if let Some(blur) = blur( - ( - settings.floating_blur_amount_x, - settings.floating_blur_amount_y, - ), - None, - None, - None, - ) { - let paint = Paint::default() - .set_anti_alias(false) - .set_blend_mode(BlendMode::Src) - .to_owned(); - let save_layer_rec = SaveLayerRec::default() - .backdrop(&blur) - .bounds(&pixel_region) - .paint(&paint); - root_canvas.save_layer(&save_layer_rec); - root_canvas.restore(); - } - } let paint = Paint::default() .set_anti_alias(false) @@ -442,7 +374,19 @@ impl RenderedWindow { let save_layer_rec = SaveLayerRec::default().bounds(&pixel_region).paint(&paint); root_canvas.save_layer(&save_layer_rec); - self.draw_surface(root_canvas, &pixel_region, grid_scale, default_background); + + let mut background_paint = Paint::default(); + background_paint.set_blend_mode(BlendMode::Src); + background_paint.set_alpha(default_background.a()); + let background_layer_rec = SaveLayerRec::default() + .bounds(&pixel_region) + .paint(&background_paint); + + root_canvas.save_layer(&background_layer_rec); + root_canvas.clear(default_background.with_a(255)); + self.draw_background_surface(root_canvas, pixel_region_box, grid_scale); + root_canvas.restore(); + self.draw_foreground_surface(root_canvas, pixel_region_box, grid_scale); root_canvas.restore(); root_canvas.restore(); @@ -450,6 +394,7 @@ impl RenderedWindow { WindowDrawDetails { id: self.id, region: pixel_region_box, + floating_order: self.anchor_info.as_ref().map(|v| v.sort_order), } } @@ -516,8 +461,8 @@ impl RenderedWindow { line_fragments, background_picture: None, foreground_picture: None, - has_transparency: false, is_inferred_border: false, + blend: 0, is_valid: false, }; @@ -693,6 +638,123 @@ impl RenderedWindow { self.scroll_delta = 0; } + fn iter_border_lines(&self) -> impl Iterator>)> { + let top_border_indices = 0..self.viewport_margins.top as isize; + let actual_line_count = self.actual_lines.len() as isize; + let bottom_border_indices = + actual_line_count - self.viewport_margins.bottom as isize..actual_line_count; + let margins_inferred = self.viewport_margins.inferred; + + top_border_indices + .chain(bottom_border_indices) + .filter_map(move |i| { + self.actual_lines[i].as_ref().and_then(|line| { + if !margins_inferred || line.borrow().is_inferred_border { + Some((i, line)) + } else { + None + } + }) + }) + } + + // Iterates over the scrollable lines (excluding the viewport margins). Includes the index for + // the given line being scrolled + fn iter_scrollable_lines(&self) -> impl Iterator>)> { + let scroll_offset_lines = self.scroll_animation.position.floor(); + let scroll_offset_lines = scroll_offset_lines as isize; + + let line_indices = if !self.scrollback_lines.is_empty() { + 0..self.grid_size.height as isize + 1 + } else { + 0..0 + }; + + line_indices.filter_map(move |i| { + self.scrollback_lines[scroll_offset_lines + i] + .as_ref() + .map(|line| (i, line)) + }) + } + + fn iter_lines(&self) -> impl Iterator>)> { + self.iter_border_lines().chain(self.iter_scrollable_lines()) + } + + fn iter_scrollable_lines_with_transform( + &self, + pixel_region: PixelRect, + grid_scale: GridScale, + ) -> impl Iterator>)> { + let scroll_offset_lines = self.scroll_animation.position.floor(); + let scroll_offset = scroll_offset_lines - self.scroll_animation.position; + let scroll_offset_pixels = (scroll_offset * grid_scale.height()).round() as isize; + + self.iter_scrollable_lines().map(move |(i, line)| { + let mut matrix = Matrix::new_identity(); + matrix.set_translate(( + pixel_region.min.x, + pixel_region.min.y + + (scroll_offset_pixels + + ((i + self.viewport_margins.top as isize) * grid_scale.height() as isize)) + as f32, + )); + (matrix, line) + }) + } + + fn iter_border_lines_with_transform( + &self, + pixel_region: PixelRect, + grid_scale: GridScale, + ) -> impl Iterator>)> { + self.iter_border_lines().map(move |(i, line)| { + let mut matrix = Matrix::new_identity(); + matrix.set_translate(( + pixel_region.min.x, + pixel_region.min.y + (i * grid_scale.height() as isize) as f32, + )); + (matrix, line) + }) + } + + /// Returns the rect containing the region of the window that does not have borders above and + /// below it. Note: This does not take into account the borders on the left and the right of + /// the window. + pub fn inner_region(&self, pixel_region: PixelRect, grid_scale: GridScale) -> Rect { + let line_height = grid_scale.height(); + let adjusted_region = PixelRect::new( + pixel_region.min + PixelVec::new(0., self.viewport_margins.top as f32 * line_height), + pixel_region.max + + PixelVec::new( + 0., + (self.viewport_margins.top - self.viewport_margins.bottom) as f32 * line_height, + ), + ); + + to_skia_rect(&adjusted_region) + } + + pub fn get_smallest_blend_value(&self) -> Option { + let height = self.grid_size.height as isize; + if height == 0 { + return None; + } + let mut smallest_blend_value: Option = None; + + for (_, line) in self.iter_lines() { + let line = line.borrow(); + line.line_fragments.iter().for_each(|f| { + if let Some(style) = &f.style { + smallest_blend_value = + Some(smallest_blend_value.map_or(style.blend, |v| v.min(style.blend))); + } + }); + } + + smallest_blend_value + } + pub fn prepare_lines(&mut self, grid_renderer: &mut GridRenderer) { let scroll_offset_lines = self.scroll_animation.position.floor() as isize; let height = self.grid_size.height as isize; @@ -713,7 +775,7 @@ impl RenderedWindow { let grid_rect = Rect::from_wh(line_size.width, line_size.height); let canvas = recorder.begin_recording(grid_rect, None); - let mut has_transparency = false; + let mut blend = 0; let mut custom_background = false; for line_fragment in line.line_fragments.iter() { @@ -731,7 +793,7 @@ impl RenderedWindow { style, ); custom_background |= background_info.custom_color; - has_transparency |= background_info.transparent; + blend = blend.min(style.as_ref().map_or(0, |s| s.blend)); } let background_picture = custom_background.then_some(recorder.finish_recording_as_picture(None).unwrap()); @@ -760,7 +822,7 @@ impl RenderedWindow { line.background_picture = background_picture; line.foreground_picture = foreground_picture; - line.has_transparency = has_transparency; + line.blend = blend; line.is_valid = true; }; diff --git a/src/units.rs b/src/units.rs index 861dd132..47dc01d2 100644 --- a/src/units.rs +++ b/src/units.rs @@ -36,6 +36,16 @@ pub fn to_skia_rect(rect: &PixelRect) -> skia_safe::Rect { #[derive(Copy, Clone)] pub struct GridScale(pub PixelSize); +impl GridScale { + pub fn height(&self) -> f32 { + self.0.height + } + + pub fn width(&self) -> f32 { + self.0.width + } +} + impl Mul for GridVec { type Output = PixelVec; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 2abf82dc..8c3d5060 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,10 @@ mod ring_buffer; +#[cfg(test)] +mod test; pub use ring_buffer::*; +#[cfg(test)] +pub use test::*; pub fn is_tty() -> bool { use std::io::IsTerminal; diff --git a/src/utils/test.rs b/src/utils/test.rs new file mode 100644 index 00000000..15dd96ef --- /dev/null +++ b/src/utils/test.rs @@ -0,0 +1,218 @@ +use std::collections::{BTreeMap, HashSet}; + +use indoc::indoc; +use lazy_static::lazy_static; +use skia_safe::{Point, Rect}; + +lazy_static! { + static ref IGNOREABLE_CHARACTERS: HashSet = + ['-', '|', '*', '+', ' '].iter().cloned().collect(); +} + +/// Helper function to convert ascii art into a list of rectangles. +/// Each rectangle must have at least two corners labeled in opposite corners to work +/// properly. +/// NOTE: This function returns rectangles that CONTAIN the labels. So when using this function in +/// conjunction with ascii_to_points, you may need to push the outer corners out by one index to +/// get the expected results +/// Returns rects in order by label. +pub fn ascii_to_rects(ascii: &str) -> Vec { + // Split the ascii into lines and characters. Loop over the characters building + // a list of coordinates by character. After all the text is iterated over, find the min + // and max coordinate for each and return them as rects. + let mut points_by_label = BTreeMap::new(); + for (y, line) in ascii.lines().enumerate() { + for (x, c) in line.chars().enumerate() { + if IGNOREABLE_CHARACTERS.contains(&c) { + continue; + } + points_by_label + .entry(c) + .or_insert_with(Vec::new) + .push((x, y)); + } + } + + let mut rects = vec![]; + for points in points_by_label.values() { + let min_x = points.iter().map(|(x, _)| x).min().unwrap(); + let min_y = points.iter().map(|(_, y)| y).min().unwrap(); + let max_x = points.iter().map(|(x, _)| x).max().unwrap(); + let max_y = points.iter().map(|(_, y)| y).max().unwrap(); + + rects.push(Rect::from_xywh( + *min_x as f32, + *min_y as f32, + (max_x - min_x + 1) as f32, + (max_y - min_y + 1) as f32, + )); + } + + rects +} + +/// Helper function to convert ascii art into a list of points ordered by their label. +pub fn ascii_to_points(ascii: &str) -> Vec { + let mut points = BTreeMap::new(); + for (y, line) in ascii.lines().enumerate() { + for (x, c) in line.chars().enumerate() { + if IGNOREABLE_CHARACTERS.contains(&c) { + continue; + } + points.entry(c).or_insert_with(Vec::new).push((x, y)); + } + } + + points + .values() + .flat_map(|points| points.iter().map(|(x, y)| Point::new(*x as f32, *y as f32))) + .collect() +} + +pub fn assert_points_eq(actual: Vec, expected_ascii: &str) { + let expected = ascii_to_points(expected_ascii); + if expected.len() != actual.len() || expected.iter().zip(actual.iter()).any(|(a, b)| a != b) { + let actual_ascii = points_to_ascii(actual); + panic!( + indoc! {" + Points do not match + Expected: + {} + Actual: + {} + "}, + expected_ascii, actual_ascii + ); + } +} + +pub fn points_to_ascii(points: Vec) -> String { + if points + .iter() + .any(|point| point.x.fract() != 0.0 || point.y.fract() != 0.) + { + panic!("Points must be integers to render as ascii"); + } + + let line_width = points.iter().map(|p| p.x as usize).max().unwrap() + 1; + let line_count = points.iter().map(|p| p.y as usize).max().unwrap() + 1; + let mut ascii = vec![vec![' '; line_width as usize]; line_count as usize]; + let numbers_big_enough = points.len() <= 9; + let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + for (index, p) in points.iter().enumerate() { + let char = if numbers_big_enough { + std::char::from_digit(index as u32, 10).unwrap() + } else { + chars.chars().nth(index).expect("Too many points") + }; + ascii[p.y as usize][p.x as usize] = char; + } + ascii + .iter() + .map(|line| line.iter().collect::()) + .collect::>() + .join("\n") +} + +#[test] +fn ascii_to_rect_works() { + // Single rect + assert_eq!( + ascii_to_rects(indoc! {" + 1---1 + | | + 1---1 + "}), + vec![Rect::from_xywh(0., 0., 5., 3.)] + ); + + // Overlapping rects + assert_eq!( + ascii_to_rects(indoc! {" + 1---1 + | 2-+-2 + 1-+-1 | + 2---2 + "}), + vec![ + Rect::from_xywh(0., 0., 5., 3.), + Rect::from_xywh(2., 1., 5., 3.), + ] + ); + + // Overlapping rects with shared corner + assert_eq!( + ascii_to_rects(indoc! {" + 1----1 + | | + *--2-1 + | | + 2--2 + "}), + vec![ + Rect::from_xywh(0., 0., 6., 3.), + Rect::from_xywh(0., 2., 4., 3.), + ] + ); + + // Adjacent rects + assert_eq!( + ascii_to_rects(indoc! {" + 1----1 + | | + 1----1 + 2--2 + | | + 2--2 + "}), + vec![ + Rect::from_xywh(0., 0., 6., 3.), + Rect::from_xywh(0., 3., 4., 3.), + ] + ); +} + +#[test] +fn ascii_to_points_works() { + // Single point + assert_eq!( + ascii_to_points(indoc! {" + 1 + "}), + vec![Point::new(0., 0.)] + ); + + // Rectangle + assert_eq!( + ascii_to_points(indoc! {" + 1-2 + | | + 3-4 + "}), + vec![ + Point::new(0., 0.), + Point::new(2., 0.), + Point::new(0., 2.), + Point::new(2., 2.), + ] + ); + + // More complicated shape + assert_eq!( + ascii_to_points(indoc! {" + 1-2 + | | + | 3-4 + | | + 6---5 + "}), + vec![ + Point::new(0., 0.), + Point::new(2., 0.), + Point::new(2., 2.), + Point::new(4., 2.), + Point::new(4., 4.), + Point::new(0., 4.), + ] + ); +}