mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-01 23:51:46 -04:00
zooming basically does what you'd expect now
This commit is contained in:
+25
-6
@@ -11,19 +11,26 @@ use kittage::{
|
||||
AsyncInputReader, ImageDimensions, ImageId, NumberOrId, PixelFormat,
|
||||
action::Action,
|
||||
delete::{ClearOrDelete, DeleteConfig, WhichToDelete},
|
||||
display::DisplayConfig,
|
||||
display::{DisplayConfig, DisplayLocation},
|
||||
error::TransmitError,
|
||||
image::Image,
|
||||
medium::Medium
|
||||
};
|
||||
use ratatui::prelude::Rect;
|
||||
use ratatui::layout::Position;
|
||||
|
||||
use crate::converter::MaybeTransferred;
|
||||
|
||||
pub struct KittyReadyToDisplay<'tui> {
|
||||
pub img: &'tui mut MaybeTransferred,
|
||||
pub page_num: usize,
|
||||
pub pos: Position,
|
||||
pub display_loc: DisplayLocation
|
||||
}
|
||||
|
||||
pub enum KittyDisplay<'tui> {
|
||||
NoChange,
|
||||
ClearImages,
|
||||
DisplayImages(Vec<(usize, &'tui mut MaybeTransferred, Rect)>)
|
||||
DisplayImages(Vec<KittyReadyToDisplay<'tui>>)
|
||||
}
|
||||
|
||||
pub struct DbgWriter<W: Write> {
|
||||
@@ -46,6 +53,7 @@ impl<W: Write> Write for DbgWriter<W> {
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
log::debug!("Writing to kitty: {:?}", self.buf);
|
||||
self.buf.clear();
|
||||
}
|
||||
self.w.flush()
|
||||
@@ -120,10 +128,19 @@ pub async fn display_kitty_images<'es>(
|
||||
};
|
||||
|
||||
let mut err = None;
|
||||
for (page_num, img, area) in images {
|
||||
let config = DisplayConfig::default();
|
||||
for KittyReadyToDisplay {
|
||||
img,
|
||||
page_num,
|
||||
pos,
|
||||
display_loc
|
||||
} in images
|
||||
{
|
||||
let config = DisplayConfig {
|
||||
location: display_loc,
|
||||
..DisplayConfig::default()
|
||||
};
|
||||
|
||||
execute!(std::io::stdout(), MoveTo(area.x, area.y)).unwrap();
|
||||
execute!(std::io::stdout(), MoveTo(pos.x, pos.y)).unwrap();
|
||||
|
||||
let this_err = match img {
|
||||
MaybeTransferred::NotYet(image) => {
|
||||
@@ -155,6 +172,8 @@ pub async fn display_kitty_images<'es>(
|
||||
|
||||
match res {
|
||||
Ok(img_id) => {
|
||||
// TODO: Re-add this or at least make sure this sort of thing does happen
|
||||
// fake_image.unlink_if_shm();
|
||||
*img = MaybeTransferred::Transferred(img_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+75
-66
@@ -2,7 +2,7 @@ use core::error::Error;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::OsString,
|
||||
io::{stdout, BufReader, Read, Stdout, Write},
|
||||
io::{stdout, BufReader, Read, Stdout},
|
||||
num::{NonZeroU32, NonZeroUsize},
|
||||
path::PathBuf
|
||||
};
|
||||
@@ -25,7 +25,7 @@ use kittage::{
|
||||
};
|
||||
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
||||
use ratatui::{Terminal, backend::CrosstermBackend};
|
||||
use ratatui_image::picker::{Picker, ProtocolType};
|
||||
use ratatui_image::{picker::{Picker, ProtocolType}, FontSize};
|
||||
use tdf::{
|
||||
PrerenderLimit,
|
||||
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
|
||||
@@ -152,68 +152,10 @@ async fn main() -> Result<(), WrappedErr> {
|
||||
})?;
|
||||
|
||||
if window_size.width == 0 || window_size.height == 0 {
|
||||
// send the command code to get the terminal window size
|
||||
print!("\x1b[14t");
|
||||
std::io::stdout().flush().unwrap();
|
||||
let (w, h) = get_font_size_through_stdio()?;
|
||||
|
||||
// we need to enable raw mode here since this bit of output won't print a newline; it'll
|
||||
// just print the info it wants to tell us. So we want to get all characters as they come
|
||||
enable_raw_mode().map_err(|e| {
|
||||
WrappedErr(
|
||||
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
|
||||
)
|
||||
})?;
|
||||
|
||||
// read in the returned size until we hit a 't' (which indicates to us it's done)
|
||||
let input_vec = BufReader::new(std::io::stdin())
|
||||
.bytes()
|
||||
.filter_map(Result::ok)
|
||||
.take_while(|b| *b != b't')
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// and then disable raw mode again in case we return an error in this next section
|
||||
disable_raw_mode().map_err(|e| {
|
||||
WrappedErr(format!("Can't put the terminal back into a normal input state: {e}").into())
|
||||
})?;
|
||||
|
||||
let input_line = String::from_utf8(input_vec).map_err(|e| {
|
||||
WrappedErr(
|
||||
format!(
|
||||
"The terminal responded to our request for its font size by providing non-utf8 data: {e}"
|
||||
)
|
||||
.into()
|
||||
)
|
||||
})?;
|
||||
let input_line = input_line
|
||||
.trim_start_matches("\x1b[4")
|
||||
.trim_start_matches(';');
|
||||
|
||||
// it should input it to us as `\e[4;<height>;<width>t`, so we need to split to get the h/w
|
||||
// ignore the first val
|
||||
let mut splits = input_line.split([';', 't']);
|
||||
|
||||
let (Some(h), Some(w)) = (splits.next(), splits.next()) else {
|
||||
return Err(WrappedErr(
|
||||
format!("Terminal responded with unparseable size response '{input_line}'").into()
|
||||
));
|
||||
};
|
||||
|
||||
window_size.height = h.parse::<u16>().map_err(|_| {
|
||||
WrappedErr(
|
||||
format!(
|
||||
"Your terminal said its height is {h}, but that is not a 16-bit unsigned integer"
|
||||
)
|
||||
.into()
|
||||
)
|
||||
})?;
|
||||
window_size.width = w.parse::<u16>().map_err(|_| {
|
||||
WrappedErr(
|
||||
format!(
|
||||
"Your terminal said its width is {w}, but that is not a 16-bit unsigned integer"
|
||||
)
|
||||
.into()
|
||||
)
|
||||
})?;
|
||||
window_size.width = w;
|
||||
window_size.height = h;
|
||||
}
|
||||
|
||||
// We need to create `picker` on this thread because if we create it on the `renderer` thread,
|
||||
@@ -248,6 +190,8 @@ async fn main() -> Result<(), WrappedErr> {
|
||||
)
|
||||
});
|
||||
|
||||
let font_size = picker.font_size();
|
||||
|
||||
let mut ev_stream = crossterm::event::EventStream::new();
|
||||
|
||||
let (to_converter, from_main) = flume::unbounded();
|
||||
@@ -326,7 +270,8 @@ async fn main() -> Result<(), WrappedErr> {
|
||||
fullscreen,
|
||||
tui,
|
||||
&mut term,
|
||||
main_area
|
||||
main_area,
|
||||
font_size
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -362,7 +307,8 @@ async fn enter_redraw_loop(
|
||||
mut fullscreen: bool,
|
||||
mut tui: Tui,
|
||||
term: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||
mut main_area: tdf::tui::RenderLayout
|
||||
mut main_area: tdf::tui::RenderLayout,
|
||||
font_size: FontSize
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
loop {
|
||||
let mut needs_redraw = true;
|
||||
@@ -424,7 +370,7 @@ async fn enter_redraw_loop(
|
||||
if needs_redraw {
|
||||
let mut to_display = KittyDisplay::NoChange;
|
||||
term.draw(|f| {
|
||||
to_display = tui.render(f, &main_area);
|
||||
to_display = tui.render(f, &main_area, font_size);
|
||||
})?;
|
||||
|
||||
let maybe_err = display_kitty_images(to_display, &mut ev_stream).await;
|
||||
@@ -499,3 +445,66 @@ fn parse_color_to_i32(cs: &str) -> Result<i32, csscolorparser::ParseColorError>
|
||||
let [r, g, b, _] = color.to_rgba8();
|
||||
Ok(i32::from_be_bytes([0, r, g, b]))
|
||||
}
|
||||
|
||||
fn get_font_size_through_stdio() -> Result<(u16, u16), WrappedErr> {
|
||||
// we need to enable raw mode here since this bit of output won't print a newline; it'll
|
||||
// just print the info it wants to tell us. So we want to get all characters as they come
|
||||
enable_raw_mode().map_err(|e| {
|
||||
WrappedErr(
|
||||
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
|
||||
)
|
||||
})?;
|
||||
|
||||
// read in the returned size until we hit a 't' (which indicates to us it's done)
|
||||
let input_vec = BufReader::new(std::io::stdin())
|
||||
.bytes()
|
||||
.filter_map(Result::ok)
|
||||
.take_while(|b| *b != b't')
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// and then disable raw mode again in case we return an error in this next section
|
||||
disable_raw_mode().map_err(|e| {
|
||||
WrappedErr(format!("Can't put the terminal back into a normal input state: {e}").into())
|
||||
})?;
|
||||
|
||||
let input_line = String::from_utf8(input_vec).map_err(|e| {
|
||||
WrappedErr(
|
||||
format!(
|
||||
"The terminal responded to our request for its font size by providing non-utf8 data: {e}"
|
||||
)
|
||||
.into()
|
||||
)
|
||||
})?;
|
||||
let input_line = input_line
|
||||
.trim_start_matches("\x1b[4")
|
||||
.trim_start_matches(';');
|
||||
|
||||
// it should input it to us as `\e[4;<height>;<width>t`, so we need to split to get the h/w
|
||||
// ignore the first val
|
||||
let mut splits = input_line.split([';', 't']);
|
||||
|
||||
let (Some(h), Some(w)) = (splits.next(), splits.next()) else {
|
||||
return Err(WrappedErr(
|
||||
format!("Terminal responded with unparseable size response '{input_line}'").into()
|
||||
));
|
||||
};
|
||||
|
||||
let h = h.parse::<u16>().map_err(|_| {
|
||||
WrappedErr(
|
||||
format!(
|
||||
"Your terminal said its height is {h}, but that is not a 16-bit unsigned integer"
|
||||
)
|
||||
.into()
|
||||
)
|
||||
})?;
|
||||
let w = w.parse::<u16>().map_err(|_| {
|
||||
WrappedErr(
|
||||
format!(
|
||||
"Your terminal said its width is {w}, but that is not a 16-bit unsigned integer"
|
||||
)
|
||||
.into()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((w, h))
|
||||
}
|
||||
|
||||
+225
-105
@@ -8,23 +8,24 @@ use crossterm::{
|
||||
enable_raw_mode
|
||||
}
|
||||
};
|
||||
use kittage::display::DisplayLocation;
|
||||
use nix::{
|
||||
sys::signal::{Signal::SIGSTOP, kill},
|
||||
unistd::Pid
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Flex, Layout, Rect},
|
||||
layout::{Constraint, Flex, Layout, Position, Rect},
|
||||
style::{Color, Style},
|
||||
symbols::border,
|
||||
text::{Span, Text},
|
||||
widgets::{Block, Borders, Clear, Padding}
|
||||
};
|
||||
use ratatui_image::Image;
|
||||
use ratatui_image::{FontSize, Image};
|
||||
|
||||
use crate::{
|
||||
converter::{ConvertedImage, MaybeTransferred},
|
||||
kitty::KittyDisplay,
|
||||
kitty::{KittyDisplay, KittyReadyToDisplay},
|
||||
renderer::{RenderError, fill_default},
|
||||
skip::Skip
|
||||
};
|
||||
@@ -39,7 +40,8 @@ pub struct Tui {
|
||||
prev_msg: Option<BottomMessage>,
|
||||
rendered: Vec<RenderedInfo>,
|
||||
page_constraints: PageConstraints,
|
||||
showing_help_msg: bool
|
||||
showing_help_msg: bool,
|
||||
zoom: Option<Zoom>
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -71,6 +73,19 @@ struct PageConstraints {
|
||||
r_to_l: bool
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
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)
|
||||
level: u16,
|
||||
// how many terminal-cells worth of content overflow the left side of the screen (and are thus
|
||||
// not displayed)
|
||||
cell_pan_from_left: u16,
|
||||
// how many terminal-cells worth of content overflow the top side of the screen (and are thus
|
||||
// not displayed)
|
||||
cell_pan_from_top: u16
|
||||
}
|
||||
|
||||
// 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
|
||||
#[derive(Default)]
|
||||
@@ -100,7 +115,8 @@ impl Tui {
|
||||
last_render: LastRender::default(),
|
||||
rendered: vec![],
|
||||
page_constraints: PageConstraints { max_wide, r_to_l },
|
||||
showing_help_msg: false
|
||||
showing_help_msg: false,
|
||||
zoom: None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,106 +149,23 @@ impl Tui {
|
||||
pub fn render<'s>(
|
||||
&'s mut self,
|
||||
frame: &mut Frame<'_>,
|
||||
full_layout: &RenderLayout
|
||||
full_layout: &RenderLayout,
|
||||
font_size: FontSize
|
||||
) -> KittyDisplay<'s> {
|
||||
if self.showing_help_msg {
|
||||
self.render_help_msg(frame);
|
||||
return KittyDisplay::ClearImages;
|
||||
}
|
||||
|
||||
if let Some((top_area, bottom_area)) = full_layout.top_and_bottom {
|
||||
let top_block = Block::new()
|
||||
.padding(Padding {
|
||||
right: 2,
|
||||
left: 2,
|
||||
..Padding::default()
|
||||
})
|
||||
.borders(Borders::BOTTOM);
|
||||
|
||||
let top_area = top_block.inner(top_area);
|
||||
|
||||
let page_nums_text = format!("{} / {}", self.page + 1, self.rendered.len());
|
||||
|
||||
let top_layout = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(page_nums_text.len() as u16)
|
||||
])
|
||||
.split(top_area);
|
||||
|
||||
let title = Span::styled(&self.name, Style::new().fg(Color::Cyan));
|
||||
|
||||
let page_nums = Span::styled(&page_nums_text, Style::new().fg(Color::Cyan));
|
||||
|
||||
frame.render_widget(top_block, top_area);
|
||||
frame.render_widget(title, top_layout[0]);
|
||||
frame.render_widget(page_nums, top_layout[1]);
|
||||
|
||||
let bottom_block = Block::new()
|
||||
.padding(Padding {
|
||||
top: 1,
|
||||
right: 2,
|
||||
left: 2,
|
||||
bottom: 0
|
||||
})
|
||||
.borders(Borders::TOP);
|
||||
let bottom_inside_block = bottom_block.inner(bottom_area);
|
||||
|
||||
frame.render_widget(bottom_block, bottom_area);
|
||||
|
||||
let rendered_str = if !self.rendered.is_empty() {
|
||||
format!(
|
||||
"Rendered: {}%",
|
||||
(self.rendered.iter().filter(|i| i.img.is_some()).count() * 100)
|
||||
/ self.rendered.len()
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let bottom_layout = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(rendered_str.len() as u16)
|
||||
])
|
||||
.split(bottom_inside_block);
|
||||
|
||||
let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan));
|
||||
frame.render_widget(rendered_span, bottom_layout[1]);
|
||||
|
||||
let (msg_str, color): (Cow<'_, str>, _) = match self.bottom_msg {
|
||||
BottomMessage::Help => ("?: Show help page".into(), Color::Blue),
|
||||
BottomMessage::Error(ref e) => (e.as_str().into(), Color::Red),
|
||||
BottomMessage::Input(ref input_state) => (
|
||||
match input_state {
|
||||
InputCommand::GoToPage(page) => format!("Go to: {page}"),
|
||||
InputCommand::Search(s) => format!("Search: {s}")
|
||||
}
|
||||
.into(),
|
||||
Color::Blue
|
||||
),
|
||||
BottomMessage::SearchResults(ref term) => {
|
||||
let num_found = self
|
||||
.rendered
|
||||
.iter()
|
||||
.filter_map(|r| r.num_results)
|
||||
.sum::<usize>();
|
||||
let num_searched = self
|
||||
.rendered
|
||||
.iter()
|
||||
.filter(|r| r.num_results.is_some())
|
||||
.count() * 100;
|
||||
(
|
||||
format!(
|
||||
"Results for '{term}': {num_found} (searched: {}%)",
|
||||
num_searched / self.rendered.len()
|
||||
)
|
||||
.into(),
|
||||
Color::Blue
|
||||
)
|
||||
}
|
||||
BottomMessage::Reloaded => ("Document was reloaded!".into(), Color::Blue)
|
||||
};
|
||||
|
||||
let span = Span::styled(msg_str, Style::new().fg(color));
|
||||
frame.render_widget(span, bottom_layout[0]);
|
||||
if let Some(t_and_b) = full_layout.top_and_bottom {
|
||||
Self::render_top_and_bottom(
|
||||
t_and_b,
|
||||
self.page,
|
||||
&self.rendered,
|
||||
&self.name,
|
||||
frame,
|
||||
&self.bottom_msg
|
||||
);
|
||||
}
|
||||
|
||||
let mut img_area = full_layout.page_area;
|
||||
@@ -246,6 +179,60 @@ impl Tui {
|
||||
frame.render_widget(Skip::new(true), img_area);
|
||||
KittyDisplay::NoChange
|
||||
} else {
|
||||
if let Some(ref 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
|
||||
// to look like what we expect.
|
||||
// See https://github.com/rust-lang/rfcs/blob/master/text/2094-nll.md#problem-case-3-conditional-control-flow-across-functions
|
||||
// You can also rewrite this to just if an `if let` and run it under
|
||||
// `RUSTFLAGS="-Zpolonius=next"` and see that it works
|
||||
if self.rendered[self.page]
|
||||
.img
|
||||
.as_ref()
|
||||
.is_some_and(|c| matches!(c, ConvertedImage::Kitty { .. }))
|
||||
{
|
||||
log::debug!("we're inside, it's kitty");
|
||||
let Some(ConvertedImage::Kitty { ref mut img, area }) =
|
||||
self.rendered[self.page].img
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let img_width = f32::from(area.width);
|
||||
let img_height = f32::from(area.height);
|
||||
let available_to_real_width_ratio = f32::from(img_area.width) / img_width;
|
||||
let available_to_real_height_ratio = f32::from(img_area.height) / img_height;
|
||||
|
||||
let (width, height) =
|
||||
if available_to_real_width_ratio > available_to_real_height_ratio {
|
||||
(img_width, img_height / available_to_real_width_ratio)
|
||||
} else {
|
||||
(img_width / available_to_real_height_ratio, img_height)
|
||||
};
|
||||
|
||||
let width = (width * f32::from(font_size.0)) as u32;
|
||||
let height = (height * f32::from(font_size.1)) as u32;
|
||||
|
||||
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()
|
||||
}
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
// here we calculate how many pages can fit in the available area.
|
||||
let mut test_area_w = img_area.width;
|
||||
// go through our pages, starting at the first one we want to view
|
||||
@@ -299,7 +286,12 @@ impl Tui {
|
||||
let maybe_img =
|
||||
Self::render_single_page(frame, img, Rect { width, ..img_area });
|
||||
img_area.x += width;
|
||||
maybe_img.map(|(img, r)| (idx + self.page, img, r))
|
||||
maybe_img.map(|(img, pos)| KittyReadyToDisplay {
|
||||
img,
|
||||
page_num: idx + self.page,
|
||||
pos,
|
||||
display_loc: DisplayLocation::default()
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -316,17 +308,15 @@ impl Tui {
|
||||
frame: &mut Frame<'_>,
|
||||
page_img: &'img mut ConvertedImage,
|
||||
img_area: Rect
|
||||
) -> Option<(&'img mut MaybeTransferred, Rect)> {
|
||||
) -> Option<(&'img mut MaybeTransferred, Position)> {
|
||||
match page_img {
|
||||
ConvertedImage::Generic(page_img) => {
|
||||
frame.render_widget(Image::new(page_img), img_area);
|
||||
None
|
||||
}
|
||||
ConvertedImage::Kitty { img, area } => Some((img, Rect {
|
||||
ConvertedImage::Kitty { img, area } => Some((img, Position {
|
||||
x: img_area.x,
|
||||
y: img_area.y,
|
||||
width: area.width,
|
||||
height: area.height
|
||||
y: img_area.y
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -410,6 +400,100 @@ impl Tui {
|
||||
self.rendered[page_num].num_results = Some(num_results);
|
||||
}
|
||||
|
||||
pub fn render_top_and_bottom(
|
||||
(top_area, bottom_area): (Rect, Rect),
|
||||
page_num: usize,
|
||||
rendered: &[RenderedInfo],
|
||||
doc_name: &str,
|
||||
frame: &mut Frame<'_>,
|
||||
bottom_msg: &BottomMessage
|
||||
) {
|
||||
let top_block = Block::new()
|
||||
.padding(Padding {
|
||||
right: 2,
|
||||
left: 2,
|
||||
..Padding::default()
|
||||
})
|
||||
.borders(Borders::BOTTOM);
|
||||
|
||||
let top_area = top_block.inner(top_area);
|
||||
|
||||
let page_nums_text = format!("{} / {}", page_num + 1, rendered.len());
|
||||
|
||||
let top_layout = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(page_nums_text.len() as u16)
|
||||
])
|
||||
.split(top_area);
|
||||
|
||||
let title = Span::styled(doc_name, Style::new().fg(Color::Cyan));
|
||||
|
||||
let page_nums = Span::styled(&page_nums_text, Style::new().fg(Color::Cyan));
|
||||
|
||||
frame.render_widget(top_block, top_area);
|
||||
frame.render_widget(title, top_layout[0]);
|
||||
frame.render_widget(page_nums, top_layout[1]);
|
||||
|
||||
let bottom_block = Block::new()
|
||||
.padding(Padding {
|
||||
top: 1,
|
||||
right: 2,
|
||||
left: 2,
|
||||
bottom: 0
|
||||
})
|
||||
.borders(Borders::TOP);
|
||||
let bottom_inside_block = bottom_block.inner(bottom_area);
|
||||
|
||||
frame.render_widget(bottom_block, bottom_area);
|
||||
|
||||
let rendered_str = if !rendered.is_empty() {
|
||||
format!(
|
||||
"Rendered: {}%",
|
||||
(rendered.iter().filter(|i| i.img.is_some()).count() * 100) / rendered.len()
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let bottom_layout = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(rendered_str.len() as u16)
|
||||
])
|
||||
.split(bottom_inside_block);
|
||||
|
||||
let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan));
|
||||
frame.render_widget(rendered_span, bottom_layout[1]);
|
||||
|
||||
let (msg_str, color): (Cow<'_, str>, _) = match bottom_msg {
|
||||
BottomMessage::Help => ("?: Show help page".into(), Color::Blue),
|
||||
BottomMessage::Error(e) => (e.as_str().into(), Color::Red),
|
||||
BottomMessage::Input(input_state) => (
|
||||
match input_state {
|
||||
InputCommand::GoToPage(page) => format!("Go to: {page}"),
|
||||
InputCommand::Search(s) => format!("Search: {s}")
|
||||
}
|
||||
.into(),
|
||||
Color::Blue
|
||||
),
|
||||
BottomMessage::SearchResults(term) => {
|
||||
let num_found = rendered.iter().filter_map(|r| r.num_results).sum::<usize>();
|
||||
let num_searched =
|
||||
rendered.iter().filter(|r| r.num_results.is_some()).count() * 100;
|
||||
(
|
||||
format!(
|
||||
"Results for '{term}': {num_found} (searched: {}%)",
|
||||
num_searched / rendered.len()
|
||||
)
|
||||
.into(),
|
||||
Color::Blue
|
||||
)
|
||||
}
|
||||
BottomMessage::Reloaded => ("Document was reloaded!".into(), Color::Blue)
|
||||
};
|
||||
|
||||
let span = Span::styled(msg_str, Style::new().fg(color));
|
||||
frame.render_widget(span, bottom_layout[0]);
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, ev: &Event) -> Option<InputAction> {
|
||||
fn jump_to_page(
|
||||
page: &mut usize,
|
||||
@@ -524,6 +608,42 @@ impl Tui {
|
||||
self.last_render.rect = Rect::default();
|
||||
Some(InputAction::Redraw)
|
||||
}
|
||||
'z' => {
|
||||
self.zoom = match self.zoom {
|
||||
None => Some(Zoom::default()),
|
||||
Some(_) => None
|
||||
};
|
||||
self.last_render.rect = Rect::default();
|
||||
Some(InputAction::Redraw)
|
||||
}
|
||||
'L' => {
|
||||
if let Some(z) = &mut self.zoom {
|
||||
z.cell_pan_from_left = z.cell_pan_from_left.saturating_add(1);
|
||||
}
|
||||
self.last_render.rect = Rect::default();
|
||||
Some(InputAction::Redraw)
|
||||
}
|
||||
'H' => {
|
||||
if let Some(z) = &mut self.zoom {
|
||||
z.cell_pan_from_left = z.cell_pan_from_left.saturating_sub(1);
|
||||
}
|
||||
self.last_render.rect = Rect::default();
|
||||
Some(InputAction::Redraw)
|
||||
}
|
||||
'J' => {
|
||||
if let Some(z) = &mut self.zoom {
|
||||
z.cell_pan_from_top = z.cell_pan_from_top.saturating_add(1);
|
||||
}
|
||||
self.last_render.rect = Rect::default();
|
||||
Some(InputAction::Redraw)
|
||||
}
|
||||
'K' => {
|
||||
if let Some(z) = &mut self.zoom {
|
||||
z.cell_pan_from_top = z.cell_pan_from_top.saturating_sub(1);
|
||||
}
|
||||
self.last_render.rect = Rect::default();
|
||||
Some(InputAction::Redraw)
|
||||
}
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user