mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
zooming basically does what you'd expect now
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
debug.log
|
||||||
|
|||||||
+25
-6
@@ -11,19 +11,26 @@ use kittage::{
|
|||||||
AsyncInputReader, ImageDimensions, ImageId, NumberOrId, PixelFormat,
|
AsyncInputReader, ImageDimensions, ImageId, NumberOrId, PixelFormat,
|
||||||
action::Action,
|
action::Action,
|
||||||
delete::{ClearOrDelete, DeleteConfig, WhichToDelete},
|
delete::{ClearOrDelete, DeleteConfig, WhichToDelete},
|
||||||
display::DisplayConfig,
|
display::{DisplayConfig, DisplayLocation},
|
||||||
error::TransmitError,
|
error::TransmitError,
|
||||||
image::Image,
|
image::Image,
|
||||||
medium::Medium
|
medium::Medium
|
||||||
};
|
};
|
||||||
use ratatui::prelude::Rect;
|
use ratatui::layout::Position;
|
||||||
|
|
||||||
use crate::converter::MaybeTransferred;
|
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> {
|
pub enum KittyDisplay<'tui> {
|
||||||
NoChange,
|
NoChange,
|
||||||
ClearImages,
|
ClearImages,
|
||||||
DisplayImages(Vec<(usize, &'tui mut MaybeTransferred, Rect)>)
|
DisplayImages(Vec<KittyReadyToDisplay<'tui>>)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DbgWriter<W: Write> {
|
pub struct DbgWriter<W: Write> {
|
||||||
@@ -46,6 +53,7 @@ impl<W: Write> Write for DbgWriter<W> {
|
|||||||
fn flush(&mut self) -> std::io::Result<()> {
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
|
log::debug!("Writing to kitty: {:?}", self.buf);
|
||||||
self.buf.clear();
|
self.buf.clear();
|
||||||
}
|
}
|
||||||
self.w.flush()
|
self.w.flush()
|
||||||
@@ -120,10 +128,19 @@ pub async fn display_kitty_images<'es>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut err = None;
|
let mut err = None;
|
||||||
for (page_num, img, area) in images {
|
for KittyReadyToDisplay {
|
||||||
let config = DisplayConfig::default();
|
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 {
|
let this_err = match img {
|
||||||
MaybeTransferred::NotYet(image) => {
|
MaybeTransferred::NotYet(image) => {
|
||||||
@@ -155,6 +172,8 @@ pub async fn display_kitty_images<'es>(
|
|||||||
|
|
||||||
match res {
|
match res {
|
||||||
Ok(img_id) => {
|
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);
|
*img = MaybeTransferred::Transferred(img_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-66
@@ -2,7 +2,7 @@ use core::error::Error;
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
io::{stdout, BufReader, Read, Stdout, Write},
|
io::{stdout, BufReader, Read, Stdout},
|
||||||
num::{NonZeroU32, NonZeroUsize},
|
num::{NonZeroU32, NonZeroUsize},
|
||||||
path::PathBuf
|
path::PathBuf
|
||||||
};
|
};
|
||||||
@@ -25,7 +25,7 @@ use kittage::{
|
|||||||
};
|
};
|
||||||
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
||||||
use ratatui::{Terminal, backend::CrosstermBackend};
|
use ratatui::{Terminal, backend::CrosstermBackend};
|
||||||
use ratatui_image::picker::{Picker, ProtocolType};
|
use ratatui_image::{picker::{Picker, ProtocolType}, FontSize};
|
||||||
use tdf::{
|
use tdf::{
|
||||||
PrerenderLimit,
|
PrerenderLimit,
|
||||||
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
|
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
|
||||||
@@ -152,68 +152,10 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if window_size.width == 0 || window_size.height == 0 {
|
if window_size.width == 0 || window_size.height == 0 {
|
||||||
// send the command code to get the terminal window size
|
let (w, h) = get_font_size_through_stdio()?;
|
||||||
print!("\x1b[14t");
|
|
||||||
std::io::stdout().flush().unwrap();
|
|
||||||
|
|
||||||
// we need to enable raw mode here since this bit of output won't print a newline; it'll
|
window_size.width = w;
|
||||||
// just print the info it wants to tell us. So we want to get all characters as they come
|
window_size.height = h;
|
||||||
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()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to create `picker` on this thread because if we create it on the `renderer` thread,
|
// 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 mut ev_stream = crossterm::event::EventStream::new();
|
||||||
|
|
||||||
let (to_converter, from_main) = flume::unbounded();
|
let (to_converter, from_main) = flume::unbounded();
|
||||||
@@ -326,7 +270,8 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
fullscreen,
|
fullscreen,
|
||||||
tui,
|
tui,
|
||||||
&mut term,
|
&mut term,
|
||||||
main_area
|
main_area,
|
||||||
|
font_size
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -362,7 +307,8 @@ async fn enter_redraw_loop(
|
|||||||
mut fullscreen: bool,
|
mut fullscreen: bool,
|
||||||
mut tui: Tui,
|
mut tui: Tui,
|
||||||
term: &mut Terminal<CrosstermBackend<Stdout>>,
|
term: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||||
mut main_area: tdf::tui::RenderLayout
|
mut main_area: tdf::tui::RenderLayout,
|
||||||
|
font_size: FontSize
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
loop {
|
loop {
|
||||||
let mut needs_redraw = true;
|
let mut needs_redraw = true;
|
||||||
@@ -424,7 +370,7 @@ async fn enter_redraw_loop(
|
|||||||
if needs_redraw {
|
if needs_redraw {
|
||||||
let mut to_display = KittyDisplay::NoChange;
|
let mut to_display = KittyDisplay::NoChange;
|
||||||
term.draw(|f| {
|
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;
|
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();
|
let [r, g, b, _] = color.to_rgba8();
|
||||||
Ok(i32::from_be_bytes([0, r, g, b]))
|
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
|
enable_raw_mode
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
use kittage::display::DisplayLocation;
|
||||||
use nix::{
|
use nix::{
|
||||||
sys::signal::{Signal::SIGSTOP, kill},
|
sys::signal::{Signal::SIGSTOP, kill},
|
||||||
unistd::Pid
|
unistd::Pid
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::{Constraint, Flex, Layout, Rect},
|
layout::{Constraint, Flex, Layout, Position, Rect},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
symbols::border,
|
symbols::border,
|
||||||
text::{Span, Text},
|
text::{Span, Text},
|
||||||
widgets::{Block, Borders, Clear, Padding}
|
widgets::{Block, Borders, Clear, Padding}
|
||||||
};
|
};
|
||||||
use ratatui_image::Image;
|
use ratatui_image::{FontSize, Image};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
converter::{ConvertedImage, MaybeTransferred},
|
converter::{ConvertedImage, MaybeTransferred},
|
||||||
kitty::KittyDisplay,
|
kitty::{KittyDisplay, KittyReadyToDisplay},
|
||||||
renderer::{RenderError, fill_default},
|
renderer::{RenderError, fill_default},
|
||||||
skip::Skip
|
skip::Skip
|
||||||
};
|
};
|
||||||
@@ -39,7 +40,8 @@ pub struct Tui {
|
|||||||
prev_msg: Option<BottomMessage>,
|
prev_msg: Option<BottomMessage>,
|
||||||
rendered: Vec<RenderedInfo>,
|
rendered: Vec<RenderedInfo>,
|
||||||
page_constraints: PageConstraints,
|
page_constraints: PageConstraints,
|
||||||
showing_help_msg: bool
|
showing_help_msg: bool,
|
||||||
|
zoom: Option<Zoom>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -71,6 +73,19 @@ struct PageConstraints {
|
|||||||
r_to_l: bool
|
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
|
// 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
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -100,7 +115,8 @@ impl Tui {
|
|||||||
last_render: LastRender::default(),
|
last_render: LastRender::default(),
|
||||||
rendered: vec![],
|
rendered: vec![],
|
||||||
page_constraints: PageConstraints { max_wide, r_to_l },
|
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>(
|
pub fn render<'s>(
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
frame: &mut Frame<'_>,
|
frame: &mut Frame<'_>,
|
||||||
full_layout: &RenderLayout
|
full_layout: &RenderLayout,
|
||||||
|
font_size: FontSize
|
||||||
) -> KittyDisplay<'s> {
|
) -> KittyDisplay<'s> {
|
||||||
if self.showing_help_msg {
|
if self.showing_help_msg {
|
||||||
self.render_help_msg(frame);
|
self.render_help_msg(frame);
|
||||||
return KittyDisplay::ClearImages;
|
return KittyDisplay::ClearImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((top_area, bottom_area)) = full_layout.top_and_bottom {
|
if let Some(t_and_b) = full_layout.top_and_bottom {
|
||||||
let top_block = Block::new()
|
Self::render_top_and_bottom(
|
||||||
.padding(Padding {
|
t_and_b,
|
||||||
right: 2,
|
self.page,
|
||||||
left: 2,
|
&self.rendered,
|
||||||
..Padding::default()
|
&self.name,
|
||||||
})
|
frame,
|
||||||
.borders(Borders::BOTTOM);
|
&self.bottom_msg
|
||||||
|
);
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut img_area = full_layout.page_area;
|
let mut img_area = full_layout.page_area;
|
||||||
@@ -246,6 +179,60 @@ impl Tui {
|
|||||||
frame.render_widget(Skip::new(true), img_area);
|
frame.render_widget(Skip::new(true), img_area);
|
||||||
KittyDisplay::NoChange
|
KittyDisplay::NoChange
|
||||||
} else {
|
} 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.
|
// here we calculate how many pages can fit in the available area.
|
||||||
let mut test_area_w = img_area.width;
|
let mut test_area_w = img_area.width;
|
||||||
// go through our pages, starting at the first one we want to view
|
// go through our pages, starting at the first one we want to view
|
||||||
@@ -299,7 +286,12 @@ impl Tui {
|
|||||||
let maybe_img =
|
let maybe_img =
|
||||||
Self::render_single_page(frame, img, Rect { width, ..img_area });
|
Self::render_single_page(frame, img, Rect { width, ..img_area });
|
||||||
img_area.x += width;
|
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<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@@ -316,17 +308,15 @@ impl Tui {
|
|||||||
frame: &mut Frame<'_>,
|
frame: &mut Frame<'_>,
|
||||||
page_img: &'img mut ConvertedImage,
|
page_img: &'img mut ConvertedImage,
|
||||||
img_area: Rect
|
img_area: Rect
|
||||||
) -> Option<(&'img mut MaybeTransferred, Rect)> {
|
) -> Option<(&'img mut MaybeTransferred, Position)> {
|
||||||
match page_img {
|
match page_img {
|
||||||
ConvertedImage::Generic(page_img) => {
|
ConvertedImage::Generic(page_img) => {
|
||||||
frame.render_widget(Image::new(page_img), img_area);
|
frame.render_widget(Image::new(page_img), img_area);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
ConvertedImage::Kitty { img, area } => Some((img, Rect {
|
ConvertedImage::Kitty { img, area } => Some((img, Position {
|
||||||
x: img_area.x,
|
x: img_area.x,
|
||||||
y: img_area.y,
|
y: img_area.y
|
||||||
width: area.width,
|
|
||||||
height: area.height
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,6 +400,100 @@ impl Tui {
|
|||||||
self.rendered[page_num].num_results = Some(num_results);
|
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> {
|
pub fn handle_event(&mut self, ev: &Event) -> Option<InputAction> {
|
||||||
fn jump_to_page(
|
fn jump_to_page(
|
||||||
page: &mut usize,
|
page: &mut usize,
|
||||||
@@ -524,6 +608,42 @@ impl Tui {
|
|||||||
self.last_render.rect = Rect::default();
|
self.last_render.rect = Rect::default();
|
||||||
Some(InputAction::Redraw)
|
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
|
_ => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user