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 <twistoy.wang@gmail.com>
Co-authored-by: Hawtian Wang <hawtian.wang@twistoy.cn>
This commit is contained in:
Kaylee Simmons 2024-04-19 13:34:31 -07:00 committed by GitHub
parent deb4dd5278
commit 278bf6bfe5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 752 additions and 158 deletions

5
.gitignore vendored
View file

@ -6,3 +6,8 @@
.DS_Store
website/book
*.AppImage
.envrc
flake.nix
flake.lock
.direnv/
.devenv/

7
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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<WindowDrawDetails> {
let pixel_regions = self
.windows
.iter()
.map(|window| window.pixel_region(grid_scale))
.collect::<Vec<_>>();
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::<Vec<_>>();
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<LayerWindow>, 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<LayerWindow>, regions: &[PixelRect<f32>]) {
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(&regions[i]), &to_skia_rect(&regions[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<Vec<&mut RenderedWindow>> {
let mut windows = windows
.into_iter()
.enumerate()
.map(|(index, window)| LayerWindow {
window,
group: index,
})
.collect::<Vec<_>>();
let regions = windows
.iter()
.map(|window| window.window.pixel_region(grid_scale))
.collect::<Vec<_>>();
group_windows_with_regions(&mut windows, &regions);
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::<Vec<_>>())
.collect_vec()
}
fn build_silhouette(regions: &[PixelRect<f32>]) -> (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)
}

View file

@ -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<LineFragment>,
background_picture: Option<Picture>,
foreground_picture: Option<Picture>,
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<f32>,
pub floating_order: Option<u64>,
}
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<i32>, grid_size: GridSize<u32>) -> 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<f32>) -> GridPos<f32> {
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<f32>,
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<RefCell<Line>>)> = 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<f32>,
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<PixelRect<f32>>,
) -> 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<Item = (isize, &Rc<RefCell<Line>>)> {
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<Item = (isize, &Rc<RefCell<Line>>)> {
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<Item = (isize, &Rc<RefCell<Line>>)> {
self.iter_border_lines().chain(self.iter_scrollable_lines())
}
fn iter_scrollable_lines_with_transform(
&self,
pixel_region: PixelRect<f32>,
grid_scale: GridScale,
) -> impl Iterator<Item = (Matrix, &Rc<RefCell<Line>>)> {
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<f32>,
grid_scale: GridScale,
) -> impl Iterator<Item = (Matrix, &Rc<RefCell<Line>>)> {
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<f32>, 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<u8> {
let height = self.grid_size.height as isize;
if height == 0 {
return None;
}
let mut smallest_blend_value: Option<u8> = 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;
};

View file

@ -36,6 +36,16 @@ pub fn to_skia_rect(rect: &PixelRect<f32>) -> skia_safe::Rect {
#[derive(Copy, Clone)]
pub struct GridScale(pub PixelSize<f32>);
impl GridScale {
pub fn height(&self) -> f32 {
self.0.height
}
pub fn width(&self) -> f32 {
self.0.width
}
}
impl Mul<GridScale> for GridVec<f32> {
type Output = PixelVec<f32>;

View file

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

218
src/utils/test.rs Normal file
View file

@ -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<char> =
['-', '|', '*', '+', ' '].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<Rect> {
// 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<Point> {
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<Point>, 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<Point>) -> 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::<String>())
.collect::<Vec<_>>()
.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.),
]
);
}