Rewrite zooming (#121)

This commit is contained in:
Max
2025-12-03 02:59:38 +01:00
committed by GitHub
parent e5cf92221f
commit 9b9796e718
+148 -81
View File
@@ -78,8 +78,8 @@ struct PageConstraints {
#[derive(Default, Debug)]
struct Zoom {
// just how much 'zoom' you have. Doesn't relate to anything specific yet, except that 0 means
// it fills the screen (instead of fits)
// just how much 'zoom' you have. 0 means it fills the screen (instead of fits), such
// that one axis is fully on-screen
level: i16,
// how many terminal-cells worth of content overflow the left side of the screen (and are thus
// not displayed)
@@ -88,6 +88,21 @@ struct Zoom {
// not displayed)
cell_pan_from_top: u16
}
impl Zoom {
/// Returns the zoom factor, where 1 is the default and means fill-screen
fn factor(&self) -> f32 {
// TODO: Make these configurable once we have a good way to set options after startup
const ZOOM_RATE: f32 = 1.1;
const ZOOM_RATE_GRANULAR: f32 = 1.05;
if self.level > 0 {
ZOOM_RATE.powi(self.level.into())
} else {
// use a more granular zoom rate for the steps between fit-screen and fill-screen
ZOOM_RATE_GRANULAR.powi(self.level.into())
}
}
}
// This seems like a kinda weird struct because it holds two optionals but any representation
// within it is valid; I think it's the best way to represent it
@@ -150,6 +165,129 @@ impl Tui {
}
}
fn render_zoomed<'s>(
// area of the 'fit-screen' page
mut img_area: Rect,
font_size: FontSize,
zoom: &mut Zoom,
img: &'s mut MaybeTransferred,
page_num: usize,
img_cell_w: u16,
img_cell_h: u16
) -> KittyDisplay<'s> {
log::debug!("zoom is {zoom:#?}");
log::debug!("page area is {img_area:#?}");
log::debug!("img dimensions are {img_cell_w}x{img_cell_h}");
// Dimensions of the section of the image to be displayed.
// Kittage calls this the "image area to display".
// We need to shrink this or the page area in order to zoom in or out,
// respectively.
let mut img_section_w = f32::from(img_cell_w);
let mut img_section_h = f32::from(img_cell_h);
let zoom_factor = zoom.factor();
if zoom_factor >= 1.0 {
// Use a smaller section of the image. This efficively zooms into that section.
img_section_w /= zoom_factor;
img_section_h /= zoom_factor;
} else {
// Shrink the page area, such that the fill-screen conversion
// will zoom out of the image.
let initial_page_w = f32::from(img_area.width);
let initial_page_h = f32::from(img_area.height);
// how many pages the image is wide/high
let img_page_w_ratio = img_section_w / initial_page_w;
let img_page_h_ratio = img_section_h / initial_page_h;
let shrink_move_page = |dim: &mut u16, pos: &mut u16, axis_zoom_factor: f32| {
let old_dim = *dim;
// The axis zoom factor tells us what portion of the axis
// we need to show.
*dim = (f32::from(*dim) * axis_zoom_factor) as u16;
*pos += old_dim
.checked_sub(*dim)
.expect("zooming out should shrink the image")
/ 2;
};
// TODO: Detect max zoom-out in zoom levels
if img_page_w_ratio < img_page_h_ratio {
// vertical scroll / tall image. zooming out means decreasing the width of the page area
shrink_move_page(
&mut img_area.width,
&mut img_area.x,
// disallow zooming out past fit-screen
zoom_factor.max(1.0 / img_page_h_ratio)
);
} else {
// horizontal scroll / wide image. zooming out means decreasing the width of the page area
shrink_move_page(
&mut img_area.height,
&mut img_area.y,
// disallow zooming out past fit-screen
zoom_factor.max(1.0 / img_page_w_ratio)
);
}
}
log::debug!("after adjustment, page area is {img_area:#?}");
// Crop the image such that in the end, the aspect ratio of the section
// is the same as that of the page area. This effectively performs the
// conversion to fill-screen.
// Note that this only works because cell_w, cell_h is in fit-screen
// format, i.e. the cell size and the page area already share at
// least one dimension.
{
let page_area_w = f32::from(img_area.width);
let page_area_h = f32::from(img_area.height);
// how many pages the image is wide/high
// Note that this is not the same as during the
// zoom-out calculation, since it changed the page
// dimensions.
let img_page_w_ratio = img_section_w / page_area_w;
let img_page_h_ratio = img_section_h / page_area_h;
if img_page_w_ratio < img_page_h_ratio {
img_section_h = page_area_h * img_page_w_ratio;
} else {
img_section_w = page_area_w * img_page_h_ratio;
}
}
let width = (img_section_w * f32::from(font_size.0)) as u32;
let height = (img_section_h * f32::from(font_size.1)) as u32;
zoom.cell_pan_from_left = zoom
.cell_pan_from_left
.min(img_cell_w.saturating_sub(img_section_w.ceil() as u16));
zoom.cell_pan_from_top = zoom
.cell_pan_from_top
.min(img_cell_h.saturating_sub(img_section_h.ceil() as u16));
KittyDisplay::DisplayImages(vec![KittyReadyToDisplay {
img,
page_num,
pos: Position {
x: img_area.x,
y: img_area.y
},
display_loc: DisplayLocation {
x: u32::from(zoom.cell_pan_from_left) * u32::from(font_size.0),
y: u32::from(zoom.cell_pan_from_top) * u32::from(font_size.1),
width,
height,
columns: img_area.width,
rows: img_area.height,
..DisplayLocation::default()
}
}])
}
// TODO: Make a way to fill the width of the screen with one page and scroll down to view it
#[must_use]
pub fn render<'s>(
@@ -183,8 +321,9 @@ impl Tui {
// be written and set to skip it so that ratatui doesn't spend a lot of time diffing it
// each re-render
frame.render_widget(Skip::new(true), img_area);
KittyDisplay::NoChange
} else {
return KittyDisplay::NoChange;
}
if let Some(ref mut zoom) = self.zoom {
// yes this is ugly and I hate it. it's due to the limitations that currently exist
// in the borrow checker. Once `-Zpolonius=next` is stabilized, we can rework this
@@ -206,84 +345,14 @@ impl Tui {
unreachable!()
};
log::debug!("zoom is now {zoom:#?}");
log::debug!("img_area is {img_area:#?}");
log::debug!("img dimensions are {cell_w}x{cell_h}");
let img_width = f32::from(cell_w);
let img_height = f32::from(cell_h);
let img_aspect_ratio = img_width / img_height;
if zoom.level < 0 {
let old_width = img_area.width;
img_area.width = img_area
.width
.saturating_sub((zoom.level * 2).unsigned_abs())
.max(
old_width
.min((f32::from(img_area.height) * img_aspect_ratio) as u16)
);
img_area.x += old_width
.checked_sub(img_area.width)
.expect("Zooming out shrinks the image")
/ 2;
log::debug!("after adjustment, img_area is {img_area:#?}");
// TODO: Find a way to detect when we've hit the maximum zoom-out and stop
// more zooming out
}
// Ugh I don't like this logic. I wish we could simplify it.
let img_area_width = f32::from(img_area.width);
let img_area_height = f32::from(img_area.height);
let available_to_real_width_ratio = img_area_width / img_width;
let available_to_real_height_ratio = img_area_height / img_height;
let (new_cell_width, new_cell_height) =
if available_to_real_width_ratio > available_to_real_height_ratio {
(img_width, img_area_height / available_to_real_width_ratio)
} else {
(img_area_width / available_to_real_height_ratio, img_height)
};
log::debug!("new_cell stuff is {new_cell_width}x{new_cell_height}");
let width = (new_cell_width * f32::from(font_size.0)) as u32;
let height = (new_cell_height * f32::from(font_size.1)) as u32;
self.last_render = LastRender {
rect: size,
pages_shown: 1,
unused_width: 0
};
zoom.cell_pan_from_left = zoom
.cell_pan_from_left
.min(cell_w.saturating_sub(new_cell_width as u16));
zoom.cell_pan_from_top = zoom
.cell_pan_from_top
.min(cell_h.saturating_sub(new_cell_height as u16));
return KittyDisplay::DisplayImages(vec![KittyReadyToDisplay {
img,
page_num: self.page,
pos: Position {
x: img_area.x,
y: img_area.y
},
display_loc: DisplayLocation {
x: u32::from(zoom.cell_pan_from_left) * u32::from(font_size.0),
y: u32::from(zoom.cell_pan_from_top) * u32::from(font_size.1),
width,
height,
columns: img_area.width,
rows: img_area.height,
..DisplayLocation::default()
}
}]);
return Self::render_zoomed(
img_area, font_size, zoom, img, self.page, cell_w, cell_h
);
}
}
@@ -368,7 +437,6 @@ impl Tui {
KittyDisplay::DisplayImages(to_display)
}
}
}
fn render_single_page<'img>(
frame: &mut Frame<'_>,
@@ -694,9 +762,8 @@ impl Tui {
self.last_render.rect = Rect::default();
Some(InputAction::SwitchRenderZoom(f_or_f))
}
'o' if self.is_kitty => self.update_zoom(|z|
// TODO: for now, we don't let people zoom in past fill-screen
z.level = z.level.saturating_add(1).min(0)),
'o' if self.is_kitty =>
self.update_zoom(|z| z.level = z.level.saturating_add(1)),
'O' if self.is_kitty =>
self.update_zoom(|z| z.level = z.level.saturating_sub(1)),
'L' if self.is_kitty => self.update_zoom(|z| {