mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
Rewrite zooming (#121)
This commit is contained in:
+148
-81
@@ -78,8 +78,8 @@ struct PageConstraints {
|
|||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
struct Zoom {
|
struct Zoom {
|
||||||
// just how much 'zoom' you have. Doesn't relate to anything specific yet, except that 0 means
|
// just how much 'zoom' you have. 0 means it fills the screen (instead of fits), such
|
||||||
// it fills the screen (instead of fits)
|
// that one axis is fully on-screen
|
||||||
level: i16,
|
level: i16,
|
||||||
// how many terminal-cells worth of content overflow the left side of the screen (and are thus
|
// how many terminal-cells worth of content overflow the left side of the screen (and are thus
|
||||||
// not displayed)
|
// not displayed)
|
||||||
@@ -88,6 +88,21 @@ struct Zoom {
|
|||||||
// not displayed)
|
// not displayed)
|
||||||
cell_pan_from_top: u16
|
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
|
// 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
|
// 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
|
// TODO: Make a way to fill the width of the screen with one page and scroll down to view it
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn render<'s>(
|
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
|
// be written and set to skip it so that ratatui doesn't spend a lot of time diffing it
|
||||||
// each re-render
|
// each re-render
|
||||||
frame.render_widget(Skip::new(true), img_area);
|
frame.render_widget(Skip::new(true), img_area);
|
||||||
KittyDisplay::NoChange
|
return KittyDisplay::NoChange;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
if let Some(ref mut zoom) = self.zoom {
|
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
|
// 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
|
// in the borrow checker. Once `-Zpolonius=next` is stabilized, we can rework this
|
||||||
@@ -206,84 +345,14 @@ impl Tui {
|
|||||||
unreachable!()
|
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 {
|
self.last_render = LastRender {
|
||||||
rect: size,
|
rect: size,
|
||||||
pages_shown: 1,
|
pages_shown: 1,
|
||||||
unused_width: 0
|
unused_width: 0
|
||||||
};
|
};
|
||||||
|
return Self::render_zoomed(
|
||||||
zoom.cell_pan_from_left = zoom
|
img_area, font_size, zoom, img, self.page, cell_w, cell_h
|
||||||
.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()
|
|
||||||
}
|
|
||||||
}]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +437,6 @@ impl Tui {
|
|||||||
KittyDisplay::DisplayImages(to_display)
|
KittyDisplay::DisplayImages(to_display)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn render_single_page<'img>(
|
fn render_single_page<'img>(
|
||||||
frame: &mut Frame<'_>,
|
frame: &mut Frame<'_>,
|
||||||
@@ -694,9 +762,8 @@ impl Tui {
|
|||||||
self.last_render.rect = Rect::default();
|
self.last_render.rect = Rect::default();
|
||||||
Some(InputAction::SwitchRenderZoom(f_or_f))
|
Some(InputAction::SwitchRenderZoom(f_or_f))
|
||||||
}
|
}
|
||||||
'o' if self.is_kitty => self.update_zoom(|z|
|
'o' if self.is_kitty =>
|
||||||
// TODO: for now, we don't let people zoom in past fill-screen
|
self.update_zoom(|z| z.level = z.level.saturating_add(1)),
|
||||||
z.level = z.level.saturating_add(1).min(0)),
|
|
||||||
'O' if self.is_kitty =>
|
'O' if self.is_kitty =>
|
||||||
self.update_zoom(|z| z.level = z.level.saturating_sub(1)),
|
self.update_zoom(|z| z.level = z.level.saturating_sub(1)),
|
||||||
'L' if self.is_kitty => self.update_zoom(|z| {
|
'L' if self.is_kitty => self.update_zoom(|z| {
|
||||||
|
|||||||
Reference in New Issue
Block a user