use crate::text::{
TabPolicy, byte_index_for_char_index, cell_width, char_index_from_cell_column,
clip_to_cells_cow,
};
use crate::window::{ColoredSpan, CursorSpec};
use crate::{Color, ColorPair, Result, Window};
#[derive(Debug, Clone, Copy)]
pub struct BorderChars {
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub horizontal: char,
pub vertical: char,
pub intersect: char,
pub intersect_left: char,
pub intersect_right: char,
pub intersect_top: char,
pub intersect_bottom: char,
}
impl BorderChars {
pub const fn single_line() -> Self {
Self {
top_left: '┌',
top_right: '┐',
bottom_left: '└',
bottom_right: '┘',
horizontal: '─',
vertical: '│',
intersect: '┼',
intersect_left: '├',
intersect_right: '┤',
intersect_top: '┬',
intersect_bottom: '┴',
}
}
pub const fn double_line() -> Self {
Self {
top_left: '╔',
top_right: '╗',
bottom_left: '╚',
bottom_right: '╝',
horizontal: '═',
vertical: '║',
intersect: '╬',
intersect_left: '╠',
intersect_right: '╣',
intersect_top: '╦',
intersect_bottom: '╩',
}
}
pub const fn ascii() -> Self {
Self {
top_left: '+',
top_right: '+',
bottom_left: '+',
bottom_right: '+',
horizontal: '-',
vertical: '|',
intersect: '+',
intersect_left: '+',
intersect_right: '+',
intersect_top: '+',
intersect_bottom: '+',
}
}
pub const fn rounded() -> Self {
Self {
top_left: '╭',
top_right: '╮',
bottom_left: '╰',
bottom_right: '╯',
horizontal: '─',
vertical: '│',
intersect: '┼',
intersect_left: '├',
intersect_right: '┤',
intersect_top: '┬',
intersect_bottom: '┴',
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FocusStyle {
pub border_chars: Option<BorderChars>,
pub border_color: Option<Color>,
pub text_color: Option<Color>,
pub bg_color: Option<Color>,
}
impl FocusStyle {
pub fn new() -> Self {
Self {
border_chars: None,
border_color: None,
text_color: None,
bg_color: None,
}
}
pub fn with_border_chars(mut self, chars: BorderChars) -> Self {
self.border_chars = Some(chars);
self
}
pub fn with_border_color(mut self, color: Color) -> Self {
self.border_color = Some(color);
self
}
pub fn with_text_color(mut self, color: Color) -> Self {
self.text_color = Some(color);
self
}
pub fn with_bg_color(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
pub fn focused() -> Self {
Self::new()
.with_border_chars(BorderChars::double_line())
.with_border_color(Color::Blue)
.with_text_color(Color::Cyan)
}
pub fn highlighted() -> Self {
Self::new()
.with_text_color(Color::Yellow)
.with_bg_color(Color::DarkGray)
}
}
impl Default for FocusStyle {
fn default() -> Self {
Self::new()
}
}
pub struct WindowView<'a> {
pub window: &'a mut dyn Window,
pub x_offset: u16,
pub y_offset: u16,
pub scroll_x: u16,
pub scroll_y: u16,
pub width: u16,
pub height: u16,
}
fn clip_view_text<'a>(
s: &'a str,
x: u16,
scroll_x: u16,
width: u16,
) -> Option<(u16, std::borrow::Cow<'a, str>)> {
if width == 0 {
return None;
}
if x >= scroll_x {
let local_x = x - scroll_x;
if local_x >= width {
return None;
}
let max_cells = width.saturating_sub(local_x);
return Some((
local_x,
clip_to_cells_cow(s, max_cells, TabPolicy::SingleCell),
));
}
let text_w = cell_width(s, TabPolicy::SingleCell);
let text_end = x.saturating_add(text_w);
if text_end <= scroll_x {
return None;
}
let visible_end = scroll_x.saturating_add(width);
let draw_cells = text_end.min(visible_end).saturating_sub(scroll_x);
let skip_cells = scroll_x - x;
let start_char = char_index_from_cell_column(s, skip_cells);
let start_byte = byte_index_for_char_index(s, start_char);
Some((
0,
clip_to_cells_cow(&s[start_byte..], draw_cells, TabPolicy::SingleCell),
))
}
impl<'a> Window for WindowView<'a> {
fn write_str(&mut self, y: u16, x: u16, s: &str) -> Result<()> {
let local_y = match y.checked_sub(self.scroll_y) {
Some(v) => v,
None => return Ok(()),
};
if local_y >= self.height {
return Ok(());
}
let Some((local_x, clipped)) = clip_view_text(s, x, self.scroll_x, self.width) else {
return Ok(());
};
self.window
.write_str(local_y + self.y_offset, local_x + self.x_offset, &clipped)
}
fn write_str_colored(&mut self, y: u16, x: u16, s: &str, colors: ColorPair) -> Result<()> {
let local_y = match y.checked_sub(self.scroll_y) {
Some(v) => v,
None => return Ok(()),
};
if local_y >= self.height {
return Ok(());
}
let Some((local_x, clipped)) = clip_view_text(s, x, self.scroll_x, self.width) else {
return Ok(());
};
self.window.write_str_colored(
local_y + self.y_offset,
local_x + self.x_offset,
&clipped,
colors,
)
}
fn write_spans_colored(&mut self, y: u16, x: u16, spans: &[ColoredSpan<'_>]) -> Result<()> {
let local_y = match y.checked_sub(self.scroll_y) {
Some(v) if v < self.height => v,
_ => return Ok(()),
};
if self.width == 0 {
return Ok(());
}
let visible_start = self.scroll_x;
let visible_end = self.scroll_x.saturating_add(self.width);
let parent_y = local_y + self.y_offset;
let mut span_start = x;
for span in spans {
let span_w = cell_width(span.text, TabPolicy::SingleCell);
let span_end = span_start.saturating_add(span_w);
if !span.text.is_empty() && span_end > visible_start && span_start < visible_end {
let skip_cells = visible_start.saturating_sub(span_start);
let draw_cells = span_end
.min(visible_end)
.saturating_sub(visible_start.max(span_start));
let start_char = char_index_from_cell_column(span.text, skip_cells);
let start_byte = byte_index_for_char_index(span.text, start_char);
let clipped =
clip_to_cells_cow(&span.text[start_byte..], draw_cells, TabPolicy::SingleCell);
if !clipped.is_empty() {
let local_x = visible_start.max(span_start) - visible_start;
self.window.write_str_colored(
parent_y,
self.x_offset + local_x,
&clipped,
span.colors,
)?;
}
}
span_start = span_end;
}
Ok(())
}
fn flush(&mut self) -> Result<()> {
self.window.flush()
}
fn request_cursor(&mut self, cursor: CursorSpec) {
let x = self
.x_offset
.saturating_add(cursor.x.saturating_sub(self.scroll_x));
let y = self
.y_offset
.saturating_add(cursor.y.saturating_sub(self.scroll_y));
self.window.request_cursor(CursorSpec {
x,
y,
visible: cursor.visible,
});
}
fn clear_cursor_request(&mut self) {
self.window.clear_cursor_request();
}
fn set_cursor_position(&mut self, x: u16, y: u16) -> Result<()> {
self.window.set_cursor_position(x, y)
}
fn show_cursor(&mut self, show: bool) -> Result<()> {
self.window.show_cursor(show)
}
fn get_size(&self) -> (u16, u16) {
(self.width, self.height)
}
fn clear_screen(&mut self) -> Result<()> {
if self.width == 0 || self.height == 0 {
return Ok(());
}
self.window.clear_area(
self.y_offset,
self.x_offset,
self.y_offset + self.height - 1,
self.x_offset + self.width - 1,
)
}
fn clear_line(&mut self, y: u16) -> Result<()> {
if self.width == 0 || self.height == 0 {
return Ok(());
}
if y < self.height {
self.window.clear_area(
self.y_offset + y,
self.x_offset,
self.y_offset + y,
self.x_offset + self.width - 1,
)
} else {
Ok(())
}
}
fn clear_area(&mut self, y1: u16, x1: u16, y2: u16, x2: u16) -> Result<()> {
if self.width == 0 || self.height == 0 {
return Ok(());
}
if x1 >= self.width || x2 >= self.width || y1 >= self.height || y2 >= self.height {
return Ok(());
}
let parent_x1 = self.x_offset + x1;
let parent_x2 = self.x_offset + x2.min(self.width - 1);
let parent_y1 = self.y_offset + y1;
let parent_y2 = self.y_offset + y2.min(self.height - 1);
self.window
.clear_area(parent_y1, parent_x1, parent_y2, parent_x2)
}
}
#[cfg(test)]
mod tests {
use super::WindowView;
use crate::{Color, ColorPair, ColoredSpan, Result, Window};
#[derive(Debug, PartialEq)]
struct Write {
y: u16,
x: u16,
text: String,
colors: Option<ColorPair>,
}
#[derive(Default)]
struct CaptureWindow {
writes: Vec<Write>,
}
impl Window for CaptureWindow {
fn write_str(&mut self, y: u16, x: u16, s: &str) -> Result<()> {
self.writes.push(Write {
y,
x,
text: s.to_string(),
colors: None,
});
Ok(())
}
fn write_str_colored(&mut self, y: u16, x: u16, s: &str, colors: ColorPair) -> Result<()> {
self.writes.push(Write {
y,
x,
text: s.to_string(),
colors: Some(colors),
});
Ok(())
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
fn set_cursor_position(&mut self, _x: u16, _y: u16) -> Result<()> {
Ok(())
}
fn show_cursor(&mut self, _show: bool) -> Result<()> {
Ok(())
}
fn get_size(&self) -> (u16, u16) {
(80, 24)
}
fn clear_screen(&mut self) -> Result<()> {
Ok(())
}
fn clear_line(&mut self, _y: u16) -> Result<()> {
Ok(())
}
fn clear_area(&mut self, _y1: u16, _x1: u16, _y2: u16, _x2: u16) -> Result<()> {
Ok(())
}
}
fn colors() -> ColorPair {
ColorPair::new(Color::White, Color::Black)
}
#[test]
fn window_view_clips_plain_text_that_starts_before_scroll_x() {
let mut parent = CaptureWindow::default();
let mut view = WindowView {
window: &mut parent,
x_offset: 10,
y_offset: 5,
scroll_x: 2,
scroll_y: 0,
width: 4,
height: 1,
};
view.write_str(0, 0, "abcdef").unwrap();
assert_eq!(
parent.writes,
vec![Write {
y: 5,
x: 10,
text: "cdef".to_string(),
colors: None,
}]
);
}
#[test]
fn window_view_clips_colored_spans_across_scroll_x() {
let mut parent = CaptureWindow::default();
let first = colors();
let second = ColorPair::new(Color::Yellow, Color::Black);
let mut view = WindowView {
window: &mut parent,
x_offset: 3,
y_offset: 2,
scroll_x: 2,
scroll_y: 0,
width: 5,
height: 1,
};
view.write_spans_colored(
0,
0,
&[
ColoredSpan::new("abcd", first),
ColoredSpan::new("efgh", second),
],
)
.unwrap();
assert_eq!(
parent.writes,
vec![
Write {
y: 2,
x: 3,
text: "cd".to_string(),
colors: Some(first),
},
Write {
y: 2,
x: 5,
text: "efg".to_string(),
colors: Some(second),
},
]
);
}
}