Skip to content

Commit

Permalink
Merge pull request #7 from preiter93/wrap
Browse files Browse the repository at this point in the history
Wrap Lines
  • Loading branch information
preiter93 authored Oct 7, 2024
2 parents eea601b + 08f6e7f commit a4421b6
Show file tree
Hide file tree
Showing 14 changed files with 418 additions and 87 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
0.7.8
0.8.0
===================
- Support for line-wrapping `EditorView::new().wrap(true);`
- Move to first ('gg') / last ('G') row
- Copy deleted line to clipboard
- Refactoring
Expand Down
13 changes: 10 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "edtui"
version = "0.7.8"
version = "0.8.0"
edition = "2021"
repository = "https://github.com/preiter93/edtui"
keywords = ["ratatui", "tui", "editor", "text", "vim"]
Expand All @@ -9,12 +9,13 @@ authors = ["preiter <[email protected]>"]
license = "MIT"

[dependencies]
ratatui = "0.28"
ratatui = { package = "ratatui", version = "0.28", features = ["unstable"] }
# jagged = { package = "edtui-jagged", path="../edtui-jagged", version = "0.1.6" }
jagged = { package = "edtui-jagged", version = "0.1.7" }
enum_dispatch = "0.3.12"
arboard = { version = "3.3.0", optional = true }
arbitrary = { version = "1", optional = true, features = ["derive"] }
unicode-width = "0.2.0"

[[example]]
name = "app"
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use ratatui::widgets::Widget;
let mut state = EditorState::default();
EditorView::new(&mut state)
.theme(EditorTheme::default())
.wrap(true) // line wrapping
.render(area, buf)
```

Expand All @@ -19,6 +20,7 @@ EditorView::new(&mut state)
- Normal, Insert and Visual mode.
- Clipboard: Uses the `arboard` clibpboard by default which allows copy pasting between the
system clipboard and the editor.
- Line wrapping

### Keybindings
`EdTUI` offers a set of keybindings similar to Vim. Here are some of the most common keybindings:
Expand Down Expand Up @@ -80,17 +82,17 @@ let event_handler = EditorEvent::default();
event_handler.on_mouse_event(mouse_event, &mut state);
```

**Note**: This feature is experimental, so expect potential bugs and breaking changes.
**Note**: This feature is experimental, so expect potential bugs and breaking changes. It does currently not work correctly on wrapped lines.

#### Roadmap

- [x] Clipboard
- [x] Search
- [x] Soft-wrap lines

- [ ] Vims `f`/`t` go to first
- [ ] Support termwiz and termion
- [ ] Display line numbers
- [ ] Remap keybindings
- [ ] Soft-wrap lines

License: MIT
7 changes: 4 additions & 3 deletions examples/app.tape
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ Output "resources/app.gif"
Set Margin 10
Set Padding 2
Set BorderRadius 10
Set Width 1200
Set Height 700
Set PlaybackSpeed 0.5
Set FontSize 46
Set Width 2300
Set Height 1200
Set PlaybackSpeed 0.4

Hide
Type "cargo run --example app"
Expand Down
3 changes: 1 addition & 2 deletions examples/app/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ Copy and paste text:
Built-in search using the '/' command.
This editor is under active development.
Don't hesitate to open issues or submit pull requests to contribute!
This editor is under active development. Don't hesitate to open issues or submit pull requests to contribute!
",
)),
event_handler: EditorEventHandler::default(),
Expand Down
24 changes: 12 additions & 12 deletions examples/app/theme.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use edtui::{EditorStatusLine, EditorTheme};
use ratatui::{
prelude::{Alignment, Stylize},
prelude::Alignment,
style::{Color, Style},
widgets::{Block, BorderType, Borders},
};
Expand All @@ -18,25 +18,25 @@ impl<'a> Theme<'a> {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Thick)
.title("Editor")
.title("|Editor|")
.title_alignment(Alignment::Center),
)
.base(Style::default().bg(DARK_BLUE).fg(WHITE))
.cursor_style(Style::default().bg(WHITE).fg(DARK_BLUE))
.selection_style(Style::default().bg(YELLOW).fg(DARK_BLUE))
.base(Style::default().bg(DARK_NIGHT).fg(WHITE))
.cursor_style(Style::default().bg(WHITE).fg(DARK_NIGHT))
.selection_style(Style::default().bg(ORANGE).fg(DARK_NIGHT))
.status_line(
EditorStatusLine::default()
.style_text(Style::default().fg(LIGHT_GRAY).bg(LIGHT_PURPLE).bold())
.style_text(Style::default().fg(LIGHT_GRAY).bg(DARK_PURPLE))
.style_text(Style::default().fg(DARK_NIGHT).bg(GREEN))
.style_line(Style::default().fg(WHITE).bg(DARK_GRAY))
.align_left(true),
),
}
}
}

pub(crate) const DARK_BLUE: Color = Color::Rgb(15, 23, 42);
pub(crate) const YELLOW: Color = Color::Rgb(250, 204, 21);
pub(crate) const DARK_GRAY: Color = Color::Rgb(16, 17, 22);
pub(crate) const WHITE: Color = Color::Rgb(248, 250, 252);
pub(crate) const LIGHT_GRAY: Color = Color::Rgb(248, 250, 252);
pub(crate) const LIGHT_PURPLE: Color = Color::Rgb(126, 34, 206);
pub(crate) const DARK_PURPLE: Color = Color::Rgb(88, 28, 135);

pub(crate) const DARK_NIGHT: Color = Color::Rgb(16, 17, 22);
pub(crate) const ORANGE: Color = Color::Rgb(255, 153, 0);
pub(crate) const GREEN: Color = Color::Rgb(0, 204, 102);
Binary file modified resources/app.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//! let mut state = EditorState::default();
//! EditorView::new(&mut state)
//! .theme(EditorTheme::default())
//! .wrap(true) // line wrapping
//! .render(area, buf)
//! ```
//!
Expand All @@ -19,6 +20,7 @@
//! - Normal, Insert and Visual mode.
//! - Clipboard: Uses the `arboard` clibpboard by default which allows copy pasting between the
//! system clipboard and the editor.
//! - Line wrapping
//!
//! ## Keybindings
//! `EdTUI` offers a set of keybindings similar to Vim. Here are some of the most common keybindings:
Expand Down Expand Up @@ -80,18 +82,18 @@
//! event_handler.on_mouse_event(mouse_event, &mut state);
//! ```
//!
//! **Note**: This feature is experimental, so expect potential bugs and breaking changes.
//! **Note**: This feature is experimental, so expect potential bugs and breaking changes. It does currently not work correctly on wrapped lines.
//!
//! ### Roadmap
//!
//! - [x] Clipboard
//! - [x] Search
//! - [x] Soft-wrap lines
//!
//! - [ ] Vims `f`/`t` go to first
//! - [ ] Support termwiz and termion
//! - [ ] Display line numbers
//! - [ ] Remap keybindings
//! - [ ] Soft-wrap lines
#![allow(
dead_code,
clippy::module_name_repetitions,
Expand Down
96 changes: 84 additions & 12 deletions src/state/view.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use crate::Index2;
use crate::{view::line_wrapper::LineWrapper, Index2, Lines};
use ratatui::layout::Rect;
use unicode_width::UnicodeWidthChar;

/// Represents the (x, y) offset of the editor's viewport.
/// It represents the top-left local editor coordinate.
#[derive(Default, Debug, Clone)]
pub(crate) struct ViewState {
/// The offset of the viewport.
pub(crate) viewport: Offset,
/// The number of rows that are displayed on the viewport
pub(crate) num_rows: usize,
/// Sets the offset from the upper-left corner of the terminal window to the start of the textarea buffer.
///
/// This offset is necessary to calculate the mouse position in relation to the text
Expand Down Expand Up @@ -54,29 +57,93 @@ impl ViewState {
&mut self,
size: (usize, usize),
cursor: Index2,
lines: &Lines,
wrap: bool,
) -> Offset {
let limit = (
let max_cursor_pos = (
size.0.saturating_sub(1) + self.viewport.x,
size.1.saturating_sub(1) + self.viewport.y,
);
// scroll left
if cursor.col < self.viewport.x {
self.viewport.x = cursor.col;
}
// scroll right
if cursor.col >= limit.0 {
self.viewport.x += cursor.col.saturating_sub(limit.0);

if wrap {
self.viewport.x = 0;
} else {
// scroll left
if cursor.col < self.viewport.x {
self.viewport.x = cursor.col;
}
// scroll right
if cursor.col > max_cursor_pos.0 {
self.viewport.x += cursor.col.saturating_sub(max_cursor_pos.0);
}
}

// scroll up
if cursor.row < self.viewport.y {
self.viewport.y = cursor.row;
}

// scroll down
if cursor.row >= limit.1 {
self.viewport.y += cursor.row.saturating_sub(limit.1);
if wrap {
self.scroll_down(lines, size.0, size.1, cursor.row);
} else if cursor.row >= max_cursor_pos.1 {
self.viewport.y += cursor.row.saturating_sub(max_cursor_pos.1);
}
self.viewport
}

/// Updates the number of rows that are currently shown on the viewport.
/// Refers to the number of editor lines, not visual lines.
pub(crate) fn update_num_rows(&mut self, num_rows: usize) {
self.num_rows = num_rows;
}

/// Scrolls the viewport down based on the cursor's row position.
///
/// This function adjusts the viewport to ensure that the cursor remains visible
/// when moving down in a list of lines. It calculates the required scrolling
/// based on the line width and wraps the content to fit within the maximum width and height.
///
/// # Behavior
///
/// If the cursor is already visible within the current viewport, no action is taken.
/// Otherwise, the function calculates how many rows the content would need to wrap,
/// and adjusts the viewport accordingly.
fn scroll_down(
&mut self,
lines: &Lines,
max_width: usize,
max_height: usize,
cursor_row: usize,
) {
// If the cursor is already within the viewport, or there are no rows to display, return early.
if cursor_row < self.viewport.y + self.num_rows || self.num_rows == 0 {
return;
}

let mut remaining_height = max_height;

let skip = lines.len().saturating_sub(cursor_row + 1);
for (i, line) in lines.iter_row().rev().skip(skip).enumerate() {
let line_width = chars_width(line);
let wrapped_rows = LineWrapper::determine_split(line_width, max_width).len();

// Subtract the number of wrapped rows from the remaining height.
remaining_height = remaining_height.saturating_sub(wrapped_rows);

// If we run out of height or exceed it, scroll the viewport.
if remaining_height == 0 {
self.viewport.y = cursor_row.saturating_sub(i - 1);
break;
}
}
}
}

fn chars_width(chars: &[char]) -> usize {
chars
.iter()
.fold(0, |sum, ch| sum + ch.width().unwrap_or(0))
}

#[cfg(test)]
Expand All @@ -96,9 +163,10 @@ mod tests {
let mut view = $given_view;
let size = $given_size;
let cursor = $given_cursor;
let lines = Lines::default();

// when
let offset = view.update_viewport_offset(size, cursor);
let offset = view.update_viewport_offset(size, cursor, &lines, false);

// then
assert_eq!(offset, $expected_offset);
Expand All @@ -114,6 +182,7 @@ mod tests {
view: ViewState{
viewport: Offset::new(0, 1),
editor_to_textarea_offset: Offset::default(),
num_rows: 0,
},
size: (1, 2),
cursor: Index2::new(0, 0),
Expand All @@ -129,6 +198,7 @@ mod tests {
view: ViewState{
viewport: Offset::new(0, 0),
editor_to_textarea_offset: Offset::default(),
num_rows: 0,
},
size: (1, 2),
cursor: Index2::new(2, 0),
Expand All @@ -141,6 +211,7 @@ mod tests {
view: ViewState{
viewport: Offset::new(1, 0),
editor_to_textarea_offset: Offset::default(),
num_rows: 0,
},
size: (2, 1),
cursor: Index2::new(0, 0),
Expand All @@ -153,6 +224,7 @@ mod tests {
view: ViewState{
viewport: Offset::new(0, 0),
editor_to_textarea_offset: Offset::default(),
num_rows: 0,
},
size: (2, 1),
cursor: Index2::new(0, 2),
Expand Down
Loading

0 comments on commit a4421b6

Please sign in to comment.