use std::convert::TryInto;
use std::iter::Iterator;
use std::marker::PhantomData;
use ropey::RopeSlice;
use tui::{
buffer::Buffer,
layout::Rect,
style::{Modifier, Style},
widgets::{Block, StatefulWidget, Widget},
};
use crate::editing::{
action::{
Action,
EditError,
EditInfo,
EditResult,
Editable,
EditorAction,
Jumpable,
PromptAction,
Promptable,
Scrollable,
Searchable,
UIResult,
},
application::{ApplicationInfo, EmptyInfo},
base::{
Axis,
CloseFlags,
Count,
MoveDir1D,
MoveDir2D,
MoveDirMod,
MovePosition,
PositionList,
ScrollSize,
ScrollStyle,
TargetShape,
ViewportContext,
WordStyle,
Wrappable,
WriteFlags,
},
buffer::{CursorGroupId, FollowersInfo, HighlightInfo},
completion::CompletionList,
context::EditContext,
cursor::Cursor,
rope::EditRope,
store::{SharedBuffer, Store},
};
use super::{ScrollActions, TerminalCursor, WindowOps};
pub struct LeftGutterInfo {
text: String,
style: Style,
}
impl LeftGutterInfo {
pub fn new(text: String, style: Style) -> Self {
LeftGutterInfo { text, style }
}
fn render(&self, area: Rect, buf: &mut Buffer) {
let _ = buf.set_stringn(area.x, area.y, &self.text, area.width as usize, self.style);
}
}
pub struct RightGutterInfo {
text: String,
style: Style,
}
impl RightGutterInfo {
pub fn new(text: String, style: Style) -> Self {
RightGutterInfo { text, style }
}
fn render(&self, area: Rect, buf: &mut Buffer) {
let _ = buf.set_stringn(area.x, area.y, &self.text, area.width as usize, self.style);
}
}
pub struct TextBoxState<I: ApplicationInfo = EmptyInfo> {
buffer: SharedBuffer<I>,
group_id: CursorGroupId,
readonly: bool,
viewctx: ViewportContext<Cursor>,
term_cursor: (u16, u16),
}
pub struct TextBox<'a, I: ApplicationInfo = EmptyInfo> {
block: Option<Block<'a>>,
prompt: &'a str,
oneline: bool,
lgutter_width: u16,
rgutter_width: u16,
_pc: PhantomData<I>,
}
fn shift_corner_nowrap(cursor: &Cursor, corner: &mut Cursor, width: usize, height: usize) {
if cursor.y < corner.y {
corner.set_y(cursor.y);
} else if cursor.y >= corner.y + height {
corner.set_y(cursor.y - height + 1);
}
if cursor.x < corner.x {
corner.set_x(cursor.x);
} else if cursor.x >= corner.x + width {
corner.set_x(cursor.x - width + 1);
}
}
fn shift_corner_wrap(cursor: &Cursor, corner: &mut Cursor, height: usize) {
if cursor.y < corner.y {
corner.set_y(cursor.y);
corner.set_x(0);
} else if cursor.y >= corner.y + height {
corner.set_y(cursor.y - height + 1);
corner.set_x(0);
} else if cursor.y == corner.y && cursor.x < corner.x {
corner.set_x(0);
}
}
fn shift_corner_oneline(cursor: &Cursor, corner: &mut Cursor) {
if cursor < corner {
corner.set_y(cursor.y);
corner.set_x(cursor.x);
}
}
fn shift_corner(
viewctx: &mut ViewportContext<Cursor>,
cursor: &Cursor,
width: usize,
height: usize,
) {
if viewctx.wrap {
shift_corner_wrap(cursor, &mut viewctx.corner, height);
} else {
shift_corner_nowrap(cursor, &mut viewctx.corner, width, height);
}
}
fn shift_cursor(cursor: &mut Cursor, corner: &Cursor, width: usize, height: usize) {
if cursor.y < corner.y {
cursor.set_y(corner.y);
} else if cursor.y >= corner.y + height {
cursor.set_y(corner.y + height - 1);
}
if cursor.x < corner.x {
cursor.set_x(corner.x);
} else if cursor.x >= corner.x + width {
cursor.set_x(corner.x + width - 1);
}
}
impl<I> TextBoxState<I>
where
I: ApplicationInfo,
{
pub fn new(buffer: SharedBuffer<I>) -> Self {
let mut viewctx = ViewportContext::default();
let group_id = buffer.write().unwrap().create_group();
viewctx.set_wrap(true);
TextBoxState {
buffer,
group_id,
readonly: false,
viewctx,
term_cursor: (0, 0),
}
}
pub fn buffer(&self) -> SharedBuffer<I> {
self.buffer.clone()
}
pub fn is_readonly(&self) -> bool {
self.readonly
}
pub fn set_readonly(&mut self, readonly: bool) {
self.readonly = readonly;
}
pub fn get(&self) -> EditRope {
self.buffer.read().unwrap().get().clone()
}
pub fn get_text(&self) -> String {
self.buffer.read().unwrap().get_text()
}
pub fn set_text<T: Into<EditRope>>(&mut self, t: T) {
self.buffer.write().unwrap().set_text(t)
}
pub fn reset(&mut self) -> EditRope {
self.buffer.write().unwrap().reset()
}
pub fn reset_text(&mut self) -> String {
self.buffer.write().unwrap().reset_text()
}
pub fn set_left_gutter(&mut self, line: usize, s: String, style: Option<Style>) {
let style = style.unwrap_or_default();
let info = LeftGutterInfo::new(s, style);
self.buffer.write().unwrap().set_line_info(line, info);
}
pub fn set_right_gutter(&mut self, line: usize, s: String, style: Option<Style>) {
let style = style.unwrap_or_default();
let info = RightGutterInfo::new(s, style);
self.buffer.write().unwrap().set_line_info(line, info);
}
pub fn set_wrap(&mut self, wrap: bool) {
self.viewctx.set_wrap(wrap);
}
pub fn set_term_info(&mut self, area: Rect) {
self.viewctx.dimensions = (area.width as usize, area.height as usize);
}
pub fn get_cursor(&mut self) -> Cursor {
self.buffer.write().unwrap().get_leader(self.group_id)
}
pub fn get_lines(&self) -> usize {
self.buffer.read().unwrap().get_lines()
}
pub fn has_lines(&self, max: usize) -> usize {
if self.viewctx.wrap {
let width = self.viewctx.get_width();
let mut count = 0;
if width == 0 {
return count;
}
let mut fline = false;
for line in self.buffer.read().unwrap().lines(0) {
let clen = line.len_chars();
count += 1;
count += clen.saturating_sub(1) / width;
fline |= clen > 0 && clen % width == 0;
if count >= max {
return max;
}
}
if fline {
count += 1;
}
return count;
} else {
self.buffer.read().unwrap().get_lines().min(max)
}
}
}
macro_rules! c2cgi {
($s: expr, $ctx: expr) => {
&($s.group_id, &$s.viewctx, $ctx)
};
}
impl<C, I> Editable<C, Store<I>, I> for TextBoxState<I>
where
C: EditContext,
I: ApplicationInfo,
{
fn editor_command(
&mut self,
act: &EditorAction,
ctx: &C,
store: &mut Store<I>,
) -> EditResult<EditInfo, I> {
if self.readonly && !act.is_readonly(ctx) {
Err(EditError::ReadOnly)
} else {
self.buffer.editor_command(act, c2cgi!(self, ctx), store)
}
}
}
impl<C, I> Jumpable<C, I> for TextBoxState<I>
where
C: EditContext,
I: ApplicationInfo,
{
fn jump(
&mut self,
list: PositionList,
dir: MoveDir1D,
count: usize,
ctx: &C,
) -> UIResult<usize, I> {
self.buffer.jump(list, dir, count, c2cgi!(self, ctx))
}
}
impl<C, I> Promptable<C, Store<I>, I> for TextBoxState<I>
where
C: EditContext,
I: ApplicationInfo,
{
fn prompt(
&mut self,
_: &PromptAction,
_: &C,
_: &mut Store<I>,
) -> EditResult<Vec<(Action<I>, C)>, I> {
Err(EditError::Failure("Not at a prompt".to_string()))
}
}
impl<C, I> Searchable<C, Store<I>, I> for TextBoxState<I>
where
C: EditContext,
I: ApplicationInfo,
{
fn search(
&mut self,
dir: MoveDirMod,
count: Count,
ctx: &C,
store: &mut Store<I>,
) -> UIResult<EditInfo, I> {
self.buffer.search(dir, count, c2cgi!(self, ctx), store)
}
}
impl<C, I> ScrollActions<C, Store<I>, I> for TextBoxState<I>
where
C: EditContext,
I: ApplicationInfo,
{
fn dirscroll(
&mut self,
dir: MoveDir2D,
size: ScrollSize,
count: &Count,
ctx: &C,
_: &mut Store<I>,
) -> EditResult<EditInfo, I> {
let count = ctx.resolve(count);
let height = self.viewctx.dimensions.1;
let rows = match size {
ScrollSize::Cell => count,
ScrollSize::HalfPage => count.saturating_mul(height) / 2,
ScrollSize::Page => count.saturating_mul(height),
};
let width = self.viewctx.dimensions.0;
let cols = match size {
ScrollSize::Cell => count,
ScrollSize::HalfPage => count.saturating_mul(width) / 2,
ScrollSize::Page => count.saturating_mul(width),
};
match (dir, self.viewctx.wrap) {
(MoveDir2D::Up, _) => self.viewctx.corner.up(rows),
(MoveDir2D::Down, _) => self.viewctx.corner.down(rows),
(MoveDir2D::Left, false) => self.viewctx.corner.left(cols),
(MoveDir2D::Right, false) => self.viewctx.corner.right(cols),
(MoveDir2D::Left | MoveDir2D::Right, true) => (),
};
let mut cursor = self.get_cursor();
let mut buffer = self.buffer.write().unwrap();
shift_cursor(&mut cursor, &self.viewctx.corner, width, height);
buffer.clamp(&mut cursor, c2cgi!(self, ctx));
shift_corner(&mut self.viewctx, &cursor, width, height);
buffer.set_leader(self.group_id, cursor);
Ok(None)
}
fn cursorpos(
&mut self,
pos: MovePosition,
axis: Axis,
_: &C,
_: &mut Store<I>,
) -> EditResult<EditInfo, I> {
if axis == Axis::Horizontal && self.viewctx.wrap {
return Ok(None);
}
let (width, height) = self.viewctx.dimensions;
let cursor = self.get_cursor();
shift_corner(&mut self.viewctx, &cursor, width, height);
match (axis, pos) {
(Axis::Horizontal, MovePosition::Beginning) => {
self.viewctx.corner.set_x(cursor.x);
},
(Axis::Horizontal, MovePosition::Middle) => {
let off = cursor.x.saturating_add(1).saturating_sub(width / 2);
self.viewctx.corner.set_x(off);
},
(Axis::Horizontal, MovePosition::End) => {
let off = cursor.x.saturating_add(1).saturating_sub(width);
self.viewctx.corner.set_x(off);
},
(Axis::Vertical, MovePosition::Beginning) => {
self.viewctx.corner.set_y(cursor.y);
},
(Axis::Vertical, MovePosition::Middle) => {
let off = cursor.y.saturating_add(1).saturating_sub(height / 2);
self.viewctx.corner.set_y(off);
},
(Axis::Vertical, MovePosition::End) => {
let off = cursor.y.saturating_add(1).saturating_sub(height);
self.viewctx.corner.set_y(off);
},
}
Ok(None)
}
fn linepos(
&mut self,
pos: MovePosition,
count: &Count,
ctx: &C,
_: &mut Store<I>,
) -> EditResult<EditInfo, I> {
let mut buffer = self.buffer.write().unwrap();
let max = buffer.get_lines();
let line = ctx.resolve(count).min(max).saturating_sub(1);
let height = self.viewctx.get_height();
buffer.set_leader(self.group_id, Cursor::new(line, 0));
match pos {
MovePosition::Beginning => {
self.viewctx.corner.set_y(line);
},
MovePosition::Middle => {
let off = line.saturating_add(1).saturating_sub(height / 2);
self.viewctx.corner.set_y(off);
},
MovePosition::End => {
let off = line.saturating_add(1).saturating_sub(height);
self.viewctx.corner.set_y(off);
},
}
Ok(None)
}
}
impl<C, I> Scrollable<C, Store<I>, I> for TextBoxState<I>
where
C: EditContext,
I: ApplicationInfo,
{
fn scroll(
&mut self,
style: &ScrollStyle,
ctx: &C,
store: &mut Store<I>,
) -> EditResult<EditInfo, I> {
match style {
ScrollStyle::Direction2D(dir, size, count) => {
return self.dirscroll(*dir, *size, count, ctx, store);
},
ScrollStyle::CursorPos(pos, axis) => {
return self.cursorpos(*pos, *axis, ctx, store);
},
ScrollStyle::LinePos(pos, count) => {
return self.linepos(*pos, count, ctx, store);
},
}
}
}
impl<I> TerminalCursor for TextBoxState<I>
where
I: ApplicationInfo,
{
fn get_term_cursor(&self) -> Option<(u16, u16)> {
if self.viewctx.get_height() == 0 {
return None;
}
self.term_cursor.into()
}
}
impl<I> WindowOps<I> for TextBoxState<I>
where
I: ApplicationInfo,
{
fn dup(&self, _: &mut Store<I>) -> Self {
let buffer = self.buffer.clone();
let group_id = buffer.write().unwrap().create_group();
TextBoxState {
buffer,
group_id,
readonly: self.readonly,
viewctx: self.viewctx.clone(),
term_cursor: (0, 0),
}
}
fn close(&mut self, _: CloseFlags, _: &mut Store<I>) -> bool {
true
}
fn write(&mut self, _: Option<&str>, _: WriteFlags, _: &mut Store<I>) -> UIResult<EditInfo, I> {
if self.readonly {
return Err(EditError::ReadOnly.into());
} else {
return Ok(None);
}
}
fn draw(&mut self, area: Rect, buf: &mut Buffer, _: bool, _: &mut Store<I>) {
TextBox::new().render(area, buf, self);
}
fn get_completions(&self) -> Option<CompletionList> {
self.buffer.read().unwrap().get_completions(self.group_id)
}
fn get_cursor_word(&self, style: &WordStyle) -> Option<String> {
self.buffer.read().unwrap().get_cursor_word(self.group_id, style)
}
fn get_selected_word(&self) -> Option<String> {
self.buffer.read().unwrap().get_selected_word(self.group_id)
}
}
impl<'a, I> TextBox<'a, I>
where
I: ApplicationInfo,
{
pub fn new() -> Self {
TextBox {
block: None,
prompt: "",
oneline: false,
lgutter_width: 0,
rgutter_width: 0,
_pc: PhantomData,
}
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn oneline(mut self) -> Self {
self.oneline = true;
self
}
pub fn prompt(mut self, prompt: &'a str) -> Self {
self.prompt = prompt;
self
}
pub fn left_gutter(mut self, lw: u16) -> Self {
self.lgutter_width = lw;
self
}
pub fn right_gutter(mut self, rw: u16) -> Self {
self.rgutter_width = rw;
self
}
#[inline]
fn _highlight_followers(
&self,
line: usize,
start: usize,
end: usize,
(x, y): (u16, u16),
followers: &FollowersInfo,
buf: &mut Buffer,
) {
let hlstyled = Style::default().add_modifier(Modifier::REVERSED);
let cs = (line, start);
let ce = (line, end);
for follower in followers.query(cs..ce) {
let fx = x + (follower.value.x - start) as u16;
let fa = Rect::new(fx, y, 1, 1);
buf.set_style(fa, hlstyled);
}
}
#[inline]
fn _set_style(&self, start: usize, h1: usize, h2: usize, (x, y): (u16, u16), buf: &mut Buffer) {
let tx: u16 = x + (h1 - start) as u16;
let selwidth: u16 = (h2 - h1 + 1).try_into().unwrap();
let hlstyled = Style::default().add_modifier(Modifier::REVERSED);
let selarea = Rect::new(tx, y, selwidth, 1);
buf.set_style(selarea, hlstyled);
}
#[inline]
fn _highlight_line(
&self,
line: usize,
start: usize,
end: usize,
(x, y): (u16, u16),
hls: &HighlightInfo,
buf: &mut Buffer,
) {
for selection in hls.query_point(line) {
let (sb, se, shape) = &selection.value;
let maxcol = end.saturating_sub(1);
let range = start..end;
match shape {
TargetShape::CharWise => {
let x1 = if line == sb.y { sb.x.max(start) } else { start };
let x2 = if line == se.y {
se.x.min(maxcol)
} else {
maxcol
};
if range.contains(&x1) && range.contains(&x2) {
self._set_style(start, x1, x2, (x, y), buf);
}
},
TargetShape::LineWise => {
let hlstyled = Style::default().add_modifier(Modifier::REVERSED);
let selwidth: u16 = (end - start).try_into().unwrap();
let selarea = Rect::new(x, y, selwidth, 1);
buf.set_style(selarea, hlstyled);
},
TargetShape::BlockWise => {
let lx = sb.x.min(se.x);
let rx = sb.x.max(se.x);
let x1 = lx.max(start);
let x2 = rx.min(maxcol);
if range.contains(&x1) && range.contains(&x2) {
self._set_style(start, x1, x2, (x, y), buf);
}
},
}
}
}
fn _render_lines_wrap(
&mut self,
area: Rect,
gutters: (Rect, Rect),
buf: &mut Buffer,
hinfo: HighlightInfo,
finfo: FollowersInfo,
state: &mut TextBoxState<I>,
) {
let bot = area.bottom();
let x = area.left();
let mut y = area.top();
let height = area.height as usize;
let width = area.width as usize;
let cursor = state.get_cursor();
shift_corner_wrap(&cursor, &mut state.viewctx.corner, height);
let cby = state.viewctx.corner.y;
let cbx = state.viewctx.corner.x;
let unstyled = Style::default();
let text = state.buffer.read().unwrap();
let mut wrapped = Vec::new();
let mut sawcursor = false;
for (loff, s) in text.lines_at(cby, cbx).enumerate() {
if wrapped.len() >= height && sawcursor {
break;
}
let base = if loff == 0 { cbx } else { 0 };
let line = cby + loff;
let mut first = true;
let mut off = 0;
let slen = s.len_chars();
while off < slen && (wrapped.len() < height || !sawcursor) {
let start = off;
let end = (start + width).min(slen);
let swrapped = s.slice(start..end).to_string();
off = end;
let start = base + start;
let end = base + end;
let slen = base + slen;
let full = end - start == width;
let last = end == slen && cursor.x == slen;
let cursor_line = line == cursor.y && ((start..end).contains(&cursor.x) || last);
if cursor_line && full && last {
wrapped.push((line, start, end, swrapped, false, first));
wrapped.push((line, end, end, " ".to_string(), true, first));
} else {
wrapped.push((line, start, end, swrapped, cursor_line, first));
}
sawcursor |= cursor_line;
first = false;
}
if slen == 0 {
let cursor_line = line == cursor.y;
wrapped.push((line, base, base, s.to_string(), cursor_line, true));
sawcursor |= cursor_line;
}
}
if wrapped.len() > height {
let n = wrapped.len() - height;
let _ = wrapped.drain(..n);
let (line, start, _, _, _, _) = wrapped.first().unwrap();
state.viewctx.corner.set_y(*line);
state.viewctx.corner.set_x(*start);
}
for (line, start, end, s, cursor_line, first) in wrapped.into_iter() {
if y >= bot {
break;
}
if first {
let lgutter = text.get_line_info::<LeftGutterInfo>(line);
let rgutter = text.get_line_info::<RightGutterInfo>(line);
if let Some(lgi) = lgutter {
let lga = Rect::new(gutters.0.x, y, gutters.0.width, 0);
lgi.render(lga, buf);
}
if let Some(rgi) = rgutter {
let rga = Rect::new(gutters.1.x, y, gutters.1.width, 0);
rgi.render(rga, buf);
}
}
let _ = buf.set_stringn(x, y, s, width, unstyled);
if cursor_line {
let coff = (cursor.x - start) as u16;
state.term_cursor = (x + coff, y);
}
self._highlight_followers(line, start, end, (x, y), &finfo, buf);
self._highlight_line(line, start, end, (x, y), &hinfo, buf);
y += 1;
}
}
fn _render_lines_oneline(
&mut self,
area: Rect,
buf: &mut Buffer,
hinfo: HighlightInfo,
finfo: FollowersInfo,
state: &mut TextBoxState<I>,
) {
let right = area.right();
let mut x = area.left();
let y = area.top();
let width = area.width as usize;
let cursor = state.get_cursor();
shift_corner_oneline(&cursor, &mut state.viewctx.corner);
let cby = state.viewctx.corner.y;
let cbx = state.viewctx.corner.x;
let unstyled = Style::default();
let text = state.buffer.read().unwrap();
let mut joined = Vec::new();
let mut sawcursor = false;
let mut len = 0;
let mut off = cbx;
for (loff, s) in text.lines_at(cby, cbx).enumerate() {
if len >= width && sawcursor {
break;
}
let base = if loff == 0 { cbx } else { 0 };
let line = cby + loff;
let slen = s.len_chars();
while off < slen && (len <= width || !sawcursor) {
let start = off;
let end = (start + width).min(slen);
let swrapped = s.slice(start..end);
off = end;
let start = base + start;
let end = base + end;
let slen = base + slen;
let full = end - start == width;
let last = end == slen && cursor.x == slen;
let cursor_line = line == cursor.y && ((start..end).contains(&cursor.x) || last);
let wlen = swrapped.len_chars();
if cursor_line && full && last {
joined.push((line, start, end, swrapped, wlen, false));
joined.push((line, end, end, RopeSlice::from(" "), 1, true));
len += wlen + 1;
} else {
joined.push((line, start, end, swrapped, wlen, cursor_line));
len += wlen;
}
sawcursor |= cursor_line;
}
if slen == 0 {
let cursor_line = line == cursor.y;
joined.push((line, 0, 0, s, 0, cursor_line));
sawcursor |= cursor_line;
}
joined.push((line, slen, slen, RopeSlice::from("^J"), 2, false));
len += 2;
off = 0;
}
if !joined.is_empty() {
joined.pop();
len -= 2;
}
if len > width {
let mut n = 0;
for (idx, (_, ref mut start, _, ref mut s, slen, ref cursor_line)) in
joined.iter_mut().enumerate()
{
if len <= width {
break;
}
let diff = len - width;
n = idx;
if *cursor_line {
let into = cursor.x - *start;
let rm = diff.min(into);
*s = s.slice(rm..);
*start += rm;
break;
} else if *slen > diff {
*s = s.slice(diff..);
*start += diff;
break;
} else {
len -= *slen;
continue;
}
}
let _ = joined.drain(..n);
let (line, start, _, _, _, _) = joined.first().unwrap();
state.viewctx.corner.set_y(*line);
state.viewctx.corner.set_x(*start);
}
state.term_cursor = (x, y);
for (line, start, end, s, _, cursor_line) in joined.into_iter() {
if x >= right {
break;
}
let s = s.to_string();
let w = (right - x) as usize;
let (xres, _) = buf.set_stringn(x, y, s, w, unstyled);
if cursor_line {
let coff = cursor.x.saturating_sub(start) as u16;
state.term_cursor = (x + coff, y);
}
self._highlight_followers(line, start, end, (x, y), &finfo, buf);
self._highlight_line(line, start, end, (x, y), &hinfo, buf);
x = xres;
}
}
fn _render_lines_nowrap(
&mut self,
area: Rect,
gutters: (Rect, Rect),
buf: &mut Buffer,
hinfo: HighlightInfo,
finfo: FollowersInfo,
state: &mut TextBoxState<I>,
) {
let bot = area.bottom();
let x = area.left();
let mut y = area.top();
let height = area.height as usize;
let width = area.width as usize;
let cursor = state.get_cursor();
shift_corner_nowrap(&cursor, &mut state.viewctx.corner, width, height);
let cby = state.viewctx.corner.y;
let cbx = state.viewctx.corner.x;
let unstyled = Style::default();
let text = state.buffer.read().unwrap();
let mut line = cby;
let mut lines = text.lines(line);
while y < bot {
if let Some(s) = lines.next() {
let lgutter = text.get_line_info::<LeftGutterInfo>(line);
let rgutter = text.get_line_info::<RightGutterInfo>(line);
let slen = s.len_chars();
let start = cbx;
let end = slen;
if let Some(lgi) = lgutter {
let lga = Rect::new(gutters.0.x, y, gutters.0.width, 0);
lgi.render(lga, buf);
}
if cbx < slen {
let _ = buf.set_stringn(x, y, s.slice(start..end).to_string(), width, unstyled);
}
if let Some(rgi) = rgutter {
let rga = Rect::new(gutters.1.x, y, gutters.1.width, 0);
rgi.render(rga, buf);
}
if line == cursor.y && (start..=end).contains(&cursor.x) {
let coff = (cursor.x - start) as u16;
state.term_cursor = (x + coff, y);
}
self._highlight_followers(line, start, end, (x, y), &finfo, buf);
self._highlight_line(line, start, end, (x, y), &hinfo, buf);
y += 1;
line += 1;
} else {
break;
}
}
}
#[inline]
fn _selection_intervals(&self, state: &mut TextBoxState<I>) -> HighlightInfo {
state.buffer.write().unwrap()._selection_intervals(state.group_id)
}
#[inline]
fn _follower_intervals(&self, state: &mut TextBoxState<I>) -> FollowersInfo {
state.buffer.write().unwrap()._follower_intervals(state.group_id)
}
fn _render_lines(&mut self, area: Rect, buf: &mut Buffer, state: &mut TextBoxState<I>) {
let hinfo = self._selection_intervals(state);
let finfo = self._follower_intervals(state);
if self.oneline {
state.set_term_info(area);
self._render_lines_oneline(area, buf, hinfo, finfo, state);
return;
}
let (lgw, rgw) = if area.width <= self.lgutter_width + self.rgutter_width {
(0, 0)
} else {
(self.lgutter_width, self.rgutter_width)
};
let textw = area.width - lgw - rgw;
let lga = Rect::new(area.x, area.y, lgw, area.height);
let texta = Rect::new(area.x + lgw, area.y, textw, area.height);
let rga = Rect::new(area.x + lgw + textw, area.y, rgw, area.height);
let gutters = (lga, rga);
state.set_term_info(texta);
if state.viewctx.wrap {
self._render_lines_wrap(texta, gutters, buf, hinfo, finfo, state);
} else {
self._render_lines_nowrap(texta, gutters, buf, hinfo, finfo, state);
}
}
}
impl<'a, I> Default for TextBox<'a, I>
where
I: ApplicationInfo,
{
fn default() -> Self {
TextBox::new()
}
}
impl<'a, I> StatefulWidget for TextBox<'a, I>
where
I: ApplicationInfo,
{
type State = TextBoxState<I>;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let area = match self.block.take() {
Some(block) => {
let inner_area = block.inner(area);
block.render(area, buf);
inner_area
},
None => area,
};
let plen = self.prompt.len() as u16;
let gutter = Rect::new(area.x, area.y, plen, area.height);
let text_area =
Rect::new(area.x + plen, area.y, area.width.saturating_sub(plen), area.height);
if text_area.width == 0 || text_area.height == 0 {
return;
}
let _ = buf.set_stringn(
gutter.left(),
gutter.top(),
self.prompt,
gutter.width as usize,
Style::default(),
);
self._render_lines(text_area, buf, state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::editing::action::{EditAction, HistoryAction};
use crate::editing::base::{EditTarget, MoveDir1D, MoveType};
use crate::editing::store::Store;
use crate::env::vim::VimContext;
macro_rules! mv {
($mt: expr) => {
EditTarget::Motion($mt, Count::Contextual)
};
($mt: expr, $c: expr) => {
EditTarget::Motion($mt, Count::Exact($c))
};
}
macro_rules! dirscroll {
($tbox: expr, $d: expr, $s: expr, $c: expr, $ctx: expr, $store: expr) => {
$tbox
.scroll(&ScrollStyle::Direction2D($d, $s, $c), $ctx, &mut $store)
.unwrap()
};
}
macro_rules! cursorpos {
($tbox: expr, $pos: expr, $axis: expr, $ctx: expr, $store: expr) => {
$tbox
.scroll(&ScrollStyle::CursorPos($pos, $axis), $ctx, &mut $store)
.unwrap()
};
}
macro_rules! linepos {
($tbox: expr, $pos: expr, $c: expr, $ctx: expr, $store: expr) => {
$tbox.scroll(&ScrollStyle::LinePos($pos, $c), $ctx, &mut $store).unwrap()
};
}
fn mkbox() -> (TextBoxState, Store<EmptyInfo>) {
let mut store = Store::default();
let buffer = store.load_buffer("".to_string());
(TextBoxState::new(buffer), store)
}
fn mkboxstr(s: &str) -> (TextBoxState, VimContext, Store<EmptyInfo>) {
let (mut b, mut store) = mkbox();
let ctx = VimContext::default();
b.set_text(s);
b.editor_command(&HistoryAction::Checkpoint.into(), &ctx, &mut store)
.unwrap();
return (b, ctx, store);
}
#[test]
fn test_scroll_dir1d() {
let (mut tbox, mut ctx, mut store) = mkboxstr(
"1234567890\n\
abcdefghij\n\
klmnopqrst\n\
uvwxyz,.<>\n\
-_=+[{]}\\|\n\
!@#$%^&*()\n\
1234567890\n",
);
tbox.set_wrap(false);
tbox.set_term_info(Rect::new(0, 0, 6, 4));
ctx.action.count = Some(4);
dirscroll!(tbox, MoveDir2D::Down, ScrollSize::Cell, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(4, 0));
assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
ctx.action.count = Some(2);
dirscroll!(tbox, MoveDir2D::Up, ScrollSize::Cell, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 0));
assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
ctx.action.count = Some(6);
dirscroll!(tbox, MoveDir2D::Right, ScrollSize::Cell, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 6));
assert_eq!(tbox.get_cursor(), Cursor::new(4, 6));
ctx.action.count = Some(2);
dirscroll!(tbox, MoveDir2D::Left, ScrollSize::Cell, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 4));
assert_eq!(tbox.get_cursor(), Cursor::new(4, 6));
ctx.action.count = None;
dirscroll!(tbox, MoveDir2D::Down, ScrollSize::HalfPage, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(4, 4));
assert_eq!(tbox.get_cursor(), Cursor::new(4, 6));
dirscroll!(tbox, MoveDir2D::Up, ScrollSize::HalfPage, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 4));
assert_eq!(tbox.get_cursor(), Cursor::new(4, 6));
dirscroll!(tbox, MoveDir2D::Right, ScrollSize::HalfPage, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 7));
assert_eq!(tbox.get_cursor(), Cursor::new(4, 7));
dirscroll!(tbox, MoveDir2D::Left, ScrollSize::HalfPage, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 4));
assert_eq!(tbox.get_cursor(), Cursor::new(4, 7));
dirscroll!(tbox, MoveDir2D::Down, ScrollSize::Page, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(6, 4));
assert_eq!(tbox.get_cursor(), Cursor::new(6, 7));
dirscroll!(tbox, MoveDir2D::Up, ScrollSize::Page, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 4));
assert_eq!(tbox.get_cursor(), Cursor::new(5, 7));
dirscroll!(tbox, MoveDir2D::Right, ScrollSize::Page, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 9));
assert_eq!(tbox.get_cursor(), Cursor::new(5, 9));
dirscroll!(tbox, MoveDir2D::Left, ScrollSize::Page, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 3));
assert_eq!(tbox.get_cursor(), Cursor::new(5, 8));
dirscroll!(tbox, MoveDir2D::Right, ScrollSize::Page, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 9));
assert_eq!(tbox.get_cursor(), Cursor::new(5, 9));
dirscroll!(tbox, MoveDir2D::Right, ScrollSize::Page, Count::Contextual, &ctx, store);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 9));
assert_eq!(tbox.get_cursor(), Cursor::new(5, 9));
}
#[test]
fn test_scroll_cursorpos() {
let (mut tbox, ctx, mut store) = mkboxstr(
"1234567890\n\
abcdefghij\n\
klmnopqrst\n\
uvwxyz,.<>\n\
-_=+[{]}\\|\n\
!@#$%^&*()\n\
1234567890\n",
);
tbox.set_wrap(false);
tbox.set_term_info(Rect::new(0, 0, 4, 4));
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
cursorpos!(tbox, MovePosition::Beginning, Axis::Vertical, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
cursorpos!(tbox, MovePosition::Middle, Axis::Vertical, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
cursorpos!(tbox, MovePosition::End, Axis::Vertical, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
cursorpos!(tbox, MovePosition::Beginning, Axis::Horizontal, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
cursorpos!(tbox, MovePosition::Middle, Axis::Horizontal, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
cursorpos!(tbox, MovePosition::End, Axis::Horizontal, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
let mov = mv!(MoveType::BufferLineOffset, 5);
let act = EditorAction::Edit(EditAction::Motion.into(), mov);
tbox.editor_command(&act, &ctx, &mut store).unwrap();
assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
let mov = mv!(MoveType::LineColumnOffset, 2);
let act = EditorAction::Edit(EditAction::Motion.into(), mov);
tbox.editor_command(&act, &ctx, &mut store).unwrap();
assert_eq!(tbox.get_cursor(), Cursor::new(4, 1));
cursorpos!(tbox, MovePosition::Beginning, Axis::Vertical, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(4, 1));
assert_eq!(tbox.viewctx.corner, Cursor::new(4, 0));
cursorpos!(tbox, MovePosition::End, Axis::Vertical, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(4, 1));
assert_eq!(tbox.viewctx.corner, Cursor::new(1, 0));
cursorpos!(tbox, MovePosition::Middle, Axis::Vertical, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(4, 1));
assert_eq!(tbox.viewctx.corner, Cursor::new(3, 0));
let mov = mv!(MoveType::LineColumnOffset, 5);
let act = EditorAction::Edit(EditAction::Motion.into(), mov);
tbox.editor_command(&act, &ctx, &mut store).unwrap();
assert_eq!(tbox.get_cursor(), Cursor::new(4, 4));
cursorpos!(tbox, MovePosition::Beginning, Axis::Horizontal, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(4, 4));
assert_eq!(tbox.viewctx.corner, Cursor::new(3, 4));
cursorpos!(tbox, MovePosition::End, Axis::Horizontal, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(4, 4));
assert_eq!(tbox.viewctx.corner, Cursor::new(3, 1));
cursorpos!(tbox, MovePosition::Middle, Axis::Horizontal, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(4, 4));
assert_eq!(tbox.viewctx.corner, Cursor::new(3, 3));
let mov = MoveType::FirstWord(MoveDir1D::Next);
let act = EditorAction::Edit(EditAction::Motion.into(), mv!(mov, 0));
tbox.editor_command(&act, &ctx, &mut store).unwrap();
cursorpos!(tbox, MovePosition::Beginning, Axis::Vertical, &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(4, 0));
}
#[test]
fn test_scroll_linepos() {
let (mut tbox, ctx, mut store) = mkboxstr(
"1234567890\n\
abcdefghij\n\
klmnopqrst\n\
uvwxyz,.<>\n\
-_=+[{]}\\|\n\
!@#$%^&*()\n\
1234567890\n",
);
tbox.set_wrap(false);
tbox.set_term_info(Rect::new(0, 0, 4, 4));
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
linepos!(tbox, MovePosition::Beginning, Count::Exact(3), &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(2, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 0));
linepos!(tbox, MovePosition::Middle, Count::Exact(7), &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(6, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(5, 0));
linepos!(tbox, MovePosition::Middle, Count::Exact(1), &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
linepos!(tbox, MovePosition::End, Count::Exact(1), &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
linepos!(tbox, MovePosition::End, Count::Exact(2), &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(1, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
linepos!(tbox, MovePosition::End, Count::Exact(3), &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(2, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
linepos!(tbox, MovePosition::End, Count::Exact(4), &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(3, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
linepos!(tbox, MovePosition::End, Count::Exact(5), &ctx, store);
assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
assert_eq!(tbox.viewctx.corner, Cursor::new(1, 0));
}
#[test]
fn test_reset_text() {
let (mut tbox, ctx, mut store) = mkboxstr("foo\nbar\nbaz");
let mov = mv!(MoveType::BufferLineOffset, 3);
let act = EditorAction::Edit(EditAction::Motion.into(), mov);
tbox.editor_command(&act, &ctx, &mut store).unwrap();
assert_eq!(tbox.get_text(), "foo\nbar\nbaz\n");
assert_eq!(tbox.get_cursor(), Cursor::new(2, 0));
assert_eq!(tbox.reset_text(), "foo\nbar\nbaz\n");
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.get_text(), "\n");
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
}
#[test]
fn test_render_nowrap() {
let (mut tbox, ctx, mut store) = mkboxstr("foo\nbar\nbaz\nquux 1 2 3 4 5");
tbox.set_wrap(false);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
let area = Rect::new(0, 8, 10, 2);
TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.get_term_cursor(), (2, 8).into());
let mov = mv!(MoveType::BufferLineOffset, 4);
let act = EditorAction::Edit(EditAction::Motion.into(), mov);
tbox.editor_command(&act, &ctx, &mut store).unwrap();
TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 0));
assert_eq!(tbox.get_cursor(), Cursor::new(3, 0));
assert_eq!(tbox.get_term_cursor(), (2, 9).into());
let mov = mv!(MoveType::LineColumnOffset, 14);
let act = EditorAction::Edit(EditAction::Motion.into(), mov);
tbox.editor_command(&act, &ctx, &mut store).unwrap();
TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
assert_eq!(tbox.viewctx.corner, Cursor::new(2, 6));
assert_eq!(tbox.get_cursor(), Cursor::new(3, 13));
assert_eq!(tbox.get_term_cursor(), (9, 9).into());
let mov = mv!(MoveType::BufferByteOffset, 0);
let act = EditorAction::Edit(EditAction::Motion.into(), mov);
tbox.editor_command(&act, &ctx, &mut store).unwrap();
TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
assert_eq!(tbox.get_term_cursor(), (2, 8).into());
}
}