use crate::app::{App, ViewMode};
use crate::core::{Chapter, ContentElement, Reader};
use crate::ui::{Component, count_lines, get_border_type};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::{
Frame,
layout::{Margin, Rect},
widgets::{Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap},
};
use ratatui_image::sliced::{SignedPosition, SlicedImage};
use ratatui_image::{StatefulImage, picker::Picker};
pub struct ReadingView;
impl Component for ReadingView {
fn render(&self, f: &mut Frame, area: Rect, app: &mut App, picker: &mut Picker) {
if let Ok(chapter) = app.reader.get_chapter(app.current_chapter) {
let width = area.width.saturating_sub(2) as usize;
let viewport_height = area.height.saturating_sub(2) as usize;
app.update_layout(width);
let mut heights = Vec::new();
for element in &chapter.elements {
heights.push(count_lines(element, width));
}
let total_height: usize = heights.iter().sum();
app.total_height = total_height;
app.viewport_height = viewport_height;
if app.scroll_to_end && total_height > viewport_height {
app.scroll = total_height.saturating_sub(viewport_height);
app.scroll_to_end = false;
}
if app.scroll_to_start {
app.scroll = 0;
app.scroll_to_start = false;
}
if app.scroll + viewport_height > total_height && total_height > viewport_height {
app.scroll = total_height.saturating_sub(viewport_height);
}
let chapter_title = format!(" {} ", chapter.title);
render_chapter_content(
f,
area,
app,
picker,
&chapter,
app.scroll,
&heights,
viewport_height,
Some(chapter_title),
true,
);
let max_scroll = total_height.saturating_sub(viewport_height);
let scroll_position = app.scroll.min(max_scroll);
let mut scrollbar_state = ScrollbarState::new(total_height)
.position(scroll_position)
.viewport_content_length(viewport_height);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓")),
area.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut scrollbar_state,
);
}
}
}
pub struct ContinuousReadingView;
impl Component for ContinuousReadingView {
fn render(&self, f: &mut Frame, area: Rect, app: &mut App, picker: &mut Picker) {
let width = area.width.saturating_sub(2) as usize;
let viewport_height = area.height.saturating_sub(2) as usize;
app.viewport_height = viewport_height;
app.update_layout(width);
if app.scroll_to_end {
let total_height = app.chapter_heights[app.current_chapter];
if total_height > viewport_height {
app.scroll = total_height.saturating_sub(viewport_height);
} else {
app.scroll = 0;
}
app.scroll_to_end = false;
}
if app.scroll_to_start {
app.scroll = 0;
app.scroll_to_start = false;
}
let mut current_y = 0;
let mut chapter_index = app.current_chapter;
while current_y < viewport_height && chapter_index < app.reader.chapter_count() {
if let Ok(chapter) = app.reader.get_chapter(chapter_index) {
let mut heights = Vec::new();
for element in &chapter.elements {
heights.push(count_lines(element, width));
}
let total_chapter_height: usize = heights.iter().sum();
let render_scroll = if chapter_index == app.current_chapter {
app.scroll
} else {
0
};
let remaining_viewport = viewport_height.saturating_sub(current_y);
let content_height = total_chapter_height.saturating_sub(render_scroll);
let visible_content_height = content_height.min(remaining_viewport);
if visible_content_height > 0
|| (chapter_index == app.current_chapter && visible_content_height == 0)
{
let chapter_area = Rect {
x: area.x + 1,
y: area.y + 1 + current_y as u16,
width: area.width.saturating_sub(2),
height: visible_content_height as u16,
};
render_chapter_content(
f,
chapter_area,
app,
picker,
&chapter,
render_scroll,
&heights,
visible_content_height,
None,
false,
);
current_y += visible_content_height;
}
chapter_index += 1;
} else {
break;
}
}
let border_type = get_border_type(&app.config);
let chapter_title = app
.chapter_titles
.get(app.current_chapter)
.cloned()
.unwrap_or_default();
let block = Block::default()
.borders(Borders::ALL)
.border_type(border_type)
.title(format!(" {} ", chapter_title));
f.render_widget(block, area);
let total_book_height = app.get_total_book_height();
let global_scroll = app.get_global_scroll();
let max_scroll = total_book_height.saturating_sub(viewport_height);
let scroll_position = global_scroll.min(max_scroll);
let mut scrollbar_state = ScrollbarState::new(total_book_height)
.position(scroll_position)
.viewport_content_length(viewport_height);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓")),
area.inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut scrollbar_state,
);
}
}
pub fn render_chapter_content(
f: &mut Frame,
area: Rect,
app: &mut App,
picker: &mut Picker,
chapter: &Chapter,
scroll: usize,
heights: &[usize],
viewport_height: usize,
title: Option<String>,
show_borders: bool,
) {
let mut current_y_offset = 0;
let content_rect = if show_borders {
area.inner(Margin {
vertical: 1,
horizontal: 1,
})
} else {
area
};
let border_type = get_border_type(&app.config);
for (i, element) in chapter.elements.iter().enumerate() {
let h = heights[i];
if current_y_offset + h > scroll && current_y_offset < scroll + viewport_height {
let rel_y = current_y_offset as i32 - scroll as i32;
match element {
ContentElement::Text(spans) => {
let start_line = if rel_y < 0 { (-rel_y) as u16 } else { 0 };
let draw_y = if rel_y < 0 { 0 } else { rel_y as u16 };
let draw_h = (h as u16)
.saturating_sub(start_line)
.min((viewport_height as u16).saturating_sub(draw_y));
if draw_h > 0 {
let area = Rect {
x: content_rect.x,
y: content_rect.y + draw_y,
width: content_rect.width,
height: draw_h,
};
let mut ratatui_spans = Vec::new();
let search_query = if !app.search_query.is_empty() {
Some(if app.search_case_sensitive {
app.search_query.clone()
} else {
app.search_query.to_lowercase()
})
} else {
None
};
let mut cumulative_char_idx = 0;
for s in spans {
let mut style = Style::default();
if s.style.bold {
style = style.add_modifier(Modifier::BOLD);
}
if s.style.italic {
style = style.add_modifier(Modifier::ITALIC);
}
if s.style.underline {
style = style.add_modifier(Modifier::UNDERLINED);
}
if s.style.strikethrough {
style = style.add_modifier(Modifier::CROSSED_OUT);
}
let mut char_spans = Vec::new();
let text_chars: Vec<char> = s.text.chars().collect();
for (c_idx, &c) in text_chars.iter().enumerate() {
let mut char_style = style;
let global_c_idx = cumulative_char_idx + c_idx;
if app.mode == ViewMode::Visual {
let (v_ei, v_ci) = app.visual_cursor;
if i == v_ei && global_c_idx == v_ci {
char_style = char_style.bg(Color::White).fg(Color::Black);
} else if let Some(anchor) = app.visual_anchor {
let (start, end) = if anchor < (v_ei, v_ci) {
(anchor, (v_ei, v_ci))
} else {
((v_ei, v_ci), anchor)
};
if (i > start.0
|| (i == start.0 && global_c_idx >= start.1))
&& (i < end.0 || (i == end.0 && global_c_idx < end.1))
{
char_style =
char_style.bg(Color::Blue).fg(Color::White);
}
}
}
char_spans.push(Span::styled(c.to_string(), char_style));
}
if let Some(ref query) = search_query {
if app.mode != ViewMode::Visual {
let text_to_search = if app.search_case_sensitive {
s.text.clone()
} else {
s.text.to_lowercase()
};
let mut last_pos = 0;
for (pos, _) in text_to_search.match_indices(query) {
if pos > last_pos {
ratatui_spans
.push(Span::styled(&s.text[last_pos..pos], style));
}
let match_end = pos + query.len();
ratatui_spans.push(Span::styled(
&s.text[pos..match_end],
style.bg(Color::Yellow).fg(Color::Black),
));
last_pos = match_end;
}
if last_pos < s.text.len() {
ratatui_spans
.push(Span::styled(&s.text[last_pos..], style));
}
} else {
ratatui_spans.extend(char_spans);
}
} else {
if app.mode == ViewMode::Visual {
ratatui_spans.extend(char_spans);
} else {
ratatui_spans.push(Span::styled(&s.text, style));
}
}
cumulative_char_idx += text_chars.len();
}
let p = Paragraph::new(Line::from(ratatui_spans))
.wrap(Wrap { trim: true })
.scroll((start_line, 0));
f.render_widget(p, area);
}
}
ContentElement::Image(path) => {
if app.config.image_type == "resize" {
let draw_y = if rel_y < 0 { 0 } else { rel_y as u16 };
let visible_h = if rel_y < 0 {
(h as i32 + rel_y).max(0) as u16
} else {
(h as u16).min((viewport_height as u16).saturating_sub(draw_y))
};
if visible_h > 0
&& let Some(protocol) = app.get_image_protocol(path, picker)
{
let area = Rect {
x: content_rect.x,
y: content_rect.y + draw_y,
width: content_rect.width,
height: visible_h,
};
f.render_stateful_widget(StatefulImage::default(), area, protocol);
}
} else if app.config.image_type == "sliced" {
let element_top = current_y_offset;
let element_bottom = current_y_offset + h;
let viewport_top = scroll;
let viewport_bottom = scroll + viewport_height;
if element_bottom > viewport_top && element_top < viewport_bottom {
let start_y = element_top.saturating_sub(viewport_top);
let end_y = (element_bottom - viewport_top).min(viewport_height);
let visible_h = end_y.saturating_sub(start_y);
if visible_h > 0 {
let image_y_offset =
(element_top as i32 - viewport_top as i32) as i16;
if let Some(sliced) = app.get_sliced_protocol(
path,
content_rect.width,
h as u16,
picker,
) {
let area = Rect {
x: content_rect.x,
y: content_rect.y + start_y as u16,
width: content_rect.width,
height: visible_h as u16,
};
let position =
SignedPosition::from((0, image_y_offset - start_y as i16));
f.render_widget(SlicedImage::new(sliced, position), area);
}
}
}
}
}
ContentElement::BlankLine => {
let draw_y = if rel_y < 0 { 0 } else { rel_y as u16 };
let visible_h = if rel_y < 0 {
(h as i32 + rel_y).max(0) as u16
} else {
(h as u16).min((viewport_height as u16).saturating_sub(draw_y))
};
if visible_h > 0 {
let area = Rect {
x: content_rect.x,
y: content_rect.y + draw_y,
width: content_rect.width,
height: visible_h,
};
let blank_paragraph =
Paragraph::new(Line::from("")).wrap(Wrap { trim: true });
f.render_widget(blank_paragraph, area);
}
}
}
}
current_y_offset += h;
}
if show_borders {
let mut block = Block::default()
.borders(Borders::ALL)
.border_type(border_type);
if let Some(t) = title {
block = block.title(t);
}
f.render_widget(block, area);
}
}