mirror of
https://github.com/helix-editor/helix
synced 2024-11-10 10:34:45 +01:00
wip: Move to new rendering structure.
This commit is contained in:
parent
29cb33300b
commit
8695415fbf
50
Cargo.lock
generated
50
Cargo.lock
generated
@ -11,9 +11,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.34"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7"
|
||||
checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
@ -195,9 +195,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.65"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15"
|
||||
checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
@ -242,15 +242,6 @@ dependencies = [
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloudabi"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "1.2.2"
|
||||
@ -615,9 +606,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.80"
|
||||
version = "0.2.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
|
||||
checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@ -777,12 +768,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b"
|
||||
checksum = "d7c6d9b8427445284a09c55be860a15855ab580a417ccad9da88f5a06787ced0"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"cloudabi",
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
@ -947,18 +937,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.117"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
|
||||
checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.117"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
|
||||
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -967,9 +957,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.59"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
|
||||
checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -1024,9 +1014,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85"
|
||||
checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
|
||||
|
||||
[[package]]
|
||||
name = "smol"
|
||||
@ -1060,9 +1050,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.53"
|
||||
version = "1.0.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68"
|
||||
checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1156,7 +1146,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tui"
|
||||
version = "0.13.0"
|
||||
source = "git+https://github.com/fdehau/tui-rs#efdd6bfb193dafcb5e3bdc75e7d2d314065da1d7"
|
||||
source = "git+https://github.com/fdehau/tui-rs#74243394d90ea1316b6bedac6c9e4f26971c76b6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
|
@ -177,7 +177,6 @@ impl Transport {
|
||||
.expect("pending_request with id not found!");
|
||||
tx.send(Err(error.into())).await?;
|
||||
}
|
||||
msg => unimplemented!("{:?}", msg),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
5
helix-syntax/languages.toml
Normal file
5
helix-syntax/languages.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[[language]]
|
||||
name = "rust"
|
||||
scope = "source.rust"
|
||||
injection-regex = "rust"
|
||||
file-types = ["rs"]
|
@ -1,13 +1,9 @@
|
||||
use crate::{
|
||||
commands,
|
||||
keymap::{self, Keymaps},
|
||||
};
|
||||
use clap::ArgMatches as Args;
|
||||
use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State};
|
||||
|
||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||
|
||||
use crate::compositor::{Component, Compositor, EventResult};
|
||||
use crate::editor_view::EditorView;
|
||||
use crate::prompt::Prompt;
|
||||
|
||||
use log::{debug, info};
|
||||
@ -37,426 +33,54 @@ use tui::{
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
|
||||
type Terminal = crate::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
|
||||
|
||||
const BASE_WIDTH: u16 = 30;
|
||||
|
||||
pub struct Application<'a> {
|
||||
pub struct Application {
|
||||
compositor: Compositor,
|
||||
editor: Editor,
|
||||
renderer: Renderer,
|
||||
terminal: Terminal,
|
||||
|
||||
executor: &'a smol::Executor<'a>,
|
||||
executor: &'static smol::Executor<'static>,
|
||||
language_server: helix_lsp::Client,
|
||||
}
|
||||
|
||||
pub struct Renderer {
|
||||
size: (u16, u16),
|
||||
terminal: Terminal,
|
||||
surface: Surface,
|
||||
cache: Surface,
|
||||
text_color: Style,
|
||||
// TODO: temp
|
||||
#[inline(always)]
|
||||
pub fn text_color() -> Style {
|
||||
return Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
// pub fn render_cursor(&mut self, view: &View, prompt: Option<&Prompt>, viewport: Rect) {
|
||||
// let mut stdout = stdout();
|
||||
// match view.doc.mode() {
|
||||
// Mode::Insert => write!(stdout, "\x1B[6 q"),
|
||||
// mode => write!(stdout, "\x1B[2 q"),
|
||||
// };
|
||||
// let pos = if let Some(prompt) = prompt {
|
||||
// Position::new(self.size.0 as usize, 2 + prompt.cursor)
|
||||
// } else {
|
||||
// let cursor = view.doc.state.selection().cursor();
|
||||
|
||||
// let mut pos = view
|
||||
// .screen_coords_at_pos(&view.doc.text().slice(..), cursor)
|
||||
// .expect("Cursor is out of bounds.");
|
||||
// pos.col += viewport.x as usize;
|
||||
// pos.row += viewport.y as usize;
|
||||
// pos
|
||||
// };
|
||||
|
||||
// execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
|
||||
// }
|
||||
|
||||
impl Application {
|
||||
pub fn new(mut args: Args, executor: &'static smol::Executor<'static>) -> Result<Self, Error> {
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
let size = terminal::size().unwrap();
|
||||
let text_color: Style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
|
||||
|
||||
let area = Rect::new(0, 0, size.0, size.1);
|
||||
|
||||
Ok(Self {
|
||||
size,
|
||||
terminal,
|
||||
surface: Surface::empty(area),
|
||||
cache: Surface::empty(area),
|
||||
text_color,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.size = (width, height);
|
||||
let area = Rect::new(0, 0, width, height);
|
||||
self.surface = Surface::empty(area);
|
||||
self.cache = Surface::empty(area);
|
||||
}
|
||||
|
||||
pub fn render_view(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
|
||||
self.render_buffer(view, viewport, theme);
|
||||
self.render_statusline(view, theme);
|
||||
}
|
||||
|
||||
// TODO: ideally not &mut View but highlights require it because of cursor cache
|
||||
pub fn render_buffer(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
|
||||
let area = Rect::new(0, 0, self.size.0, self.size.1);
|
||||
|
||||
// clear with background color
|
||||
self.surface.set_style(area, theme.get("ui.background"));
|
||||
|
||||
// TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
|
||||
let source_code = view.doc.text().to_string();
|
||||
|
||||
let last_line = view.last_line();
|
||||
|
||||
let range = {
|
||||
// calculate viewport byte ranges
|
||||
let start = view.doc.text().line_to_byte(view.first_line);
|
||||
let end = view.doc.text().line_to_byte(last_line)
|
||||
+ view.doc.text().line(last_line).len_bytes();
|
||||
|
||||
start..end
|
||||
};
|
||||
|
||||
// TODO: range doesn't actually restrict source, just highlight range
|
||||
// TODO: cache highlight results
|
||||
// TODO: only recalculate when state.doc is actually modified
|
||||
let highlights: Vec<_> = match view.doc.syntax.as_mut() {
|
||||
Some(syntax) => {
|
||||
syntax
|
||||
.highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
|
||||
.unwrap()
|
||||
.collect() // TODO: we collect here to avoid double borrow, fix later
|
||||
}
|
||||
None => vec![Ok(HighlightEvent::Source {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
})],
|
||||
};
|
||||
let mut spans = Vec::new();
|
||||
let mut visual_x = 0;
|
||||
let mut line = 0u16;
|
||||
let visible_selections: Vec<Range> = view
|
||||
.doc
|
||||
.state
|
||||
.selection()
|
||||
.ranges()
|
||||
.iter()
|
||||
// TODO: limit selection to one in viewport
|
||||
// .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
'outer: for event in highlights {
|
||||
match event.unwrap() {
|
||||
HighlightEvent::HighlightStart(span) => {
|
||||
spans.push(span);
|
||||
}
|
||||
HighlightEvent::HighlightEnd => {
|
||||
spans.pop();
|
||||
}
|
||||
HighlightEvent::Source { start, end } => {
|
||||
// TODO: filter out spans out of viewport for now..
|
||||
|
||||
let start = view.doc.text().byte_to_char(start);
|
||||
let end = view.doc.text().byte_to_char(end); // <-- index 744, len 743
|
||||
|
||||
let text = view.doc.text().slice(start..end);
|
||||
|
||||
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
|
||||
|
||||
let style = match spans.first() {
|
||||
Some(span) => theme.get(theme.scopes()[span.0].as_str()),
|
||||
None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
|
||||
};
|
||||
|
||||
// TODO: we could render the text to a surface, then cache that, that
|
||||
// way if only the selection/cursor changes we can copy from cache
|
||||
// and paint the new cursor.
|
||||
|
||||
let mut char_index = start;
|
||||
|
||||
// iterate over range char by char
|
||||
for grapheme in RopeGraphemes::new(&text) {
|
||||
// TODO: track current char_index
|
||||
|
||||
if grapheme == "\n" {
|
||||
visual_x = 0;
|
||||
line += 1;
|
||||
|
||||
// TODO: with proper iter this shouldn't be necessary
|
||||
if line >= viewport.height {
|
||||
break 'outer;
|
||||
}
|
||||
} else if grapheme == "\t" {
|
||||
visual_x += (TAB_WIDTH as u16);
|
||||
} else {
|
||||
// Cow will prevent allocations if span contained in a single slice
|
||||
// which should really be the majority case
|
||||
let grapheme = Cow::from(grapheme);
|
||||
let width = grapheme_width(&grapheme) as u16;
|
||||
|
||||
// TODO: this should really happen as an after pass
|
||||
let style = if visible_selections
|
||||
.iter()
|
||||
.any(|range| range.contains(char_index))
|
||||
{
|
||||
// cedar
|
||||
style.clone().bg(Color::Rgb(128, 47, 0))
|
||||
} else {
|
||||
style
|
||||
};
|
||||
|
||||
let style = if visible_selections
|
||||
.iter()
|
||||
.any(|range| range.head == char_index)
|
||||
{
|
||||
style.clone().bg(Color::Rgb(255, 255, 255))
|
||||
} else {
|
||||
style
|
||||
};
|
||||
|
||||
// ugh, improve with a traverse method
|
||||
// or interleave highlight spans with selection and diagnostic spans
|
||||
let style = if view.doc.diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index
|
||||
}) {
|
||||
style.clone().add_modifier(Modifier::UNDERLINED)
|
||||
} else {
|
||||
style
|
||||
};
|
||||
|
||||
// TODO: paint cursor heads except primary
|
||||
|
||||
self.surface.set_string(
|
||||
viewport.x + visual_x,
|
||||
viewport.y + line,
|
||||
grapheme,
|
||||
style,
|
||||
);
|
||||
|
||||
visual_x += width;
|
||||
}
|
||||
|
||||
char_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let style: Style = theme.get("ui.linenr");
|
||||
let warning: Style = theme.get("warning");
|
||||
let last_line = view.last_line();
|
||||
for (i, line) in (view.first_line..last_line).enumerate() {
|
||||
if view.doc.diagnostics.iter().any(|d| d.line == line) {
|
||||
self.surface.set_stringn(0, i as u16, "●", 1, warning);
|
||||
}
|
||||
|
||||
self.surface
|
||||
.set_stringn(1, i as u16, format!("{:>5}", line + 1), 5, style);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_statusline(&mut self, view: &View, theme: &Theme) {
|
||||
let mode = match view.doc.mode() {
|
||||
Mode::Insert => "INS",
|
||||
Mode::Normal => "NOR",
|
||||
Mode::Goto => "GOTO",
|
||||
};
|
||||
// statusline
|
||||
self.surface.set_style(
|
||||
Rect::new(0, self.size.1 - 2, self.size.0, 1),
|
||||
theme.get("ui.statusline"),
|
||||
);
|
||||
self.surface
|
||||
.set_string(1, self.size.1 - 2, mode, self.text_color);
|
||||
|
||||
if let Some(path) = view.doc.path() {
|
||||
self.surface
|
||||
.set_string(6, self.size.1 - 2, path.to_string_lossy(), self.text_color);
|
||||
}
|
||||
|
||||
self.surface.set_string(
|
||||
self.size.0 - 10,
|
||||
self.size.1 - 2,
|
||||
format!("{}", view.doc.diagnostics.len()),
|
||||
self.text_color,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn render_prompt(&mut self, prompt: &Prompt, theme: &Theme) {
|
||||
// completion
|
||||
if !prompt.completion.is_empty() {
|
||||
// TODO: find out better way of clearing individual lines of the screen
|
||||
let mut row = 0;
|
||||
let mut col = 0;
|
||||
let max_col = self.size.0 / BASE_WIDTH;
|
||||
let col_height = ((prompt.completion.len() as u16 + max_col - 1) / max_col);
|
||||
|
||||
for i in (3..col_height + 3) {
|
||||
self.surface.set_string(
|
||||
0,
|
||||
self.size.1 - i as u16,
|
||||
" ".repeat(self.size.0 as usize),
|
||||
self.text_color,
|
||||
);
|
||||
}
|
||||
self.surface.set_style(
|
||||
Rect::new(0, self.size.1 - col_height - 2, self.size.0, col_height),
|
||||
theme.get("ui.statusline"),
|
||||
);
|
||||
for (i, command) in prompt.completion.iter().enumerate() {
|
||||
let color = if prompt.completion_selection_index.is_some()
|
||||
&& i == prompt.completion_selection_index.unwrap()
|
||||
{
|
||||
Style::default().bg(Color::Rgb(104, 060, 232))
|
||||
} else {
|
||||
self.text_color
|
||||
};
|
||||
self.surface.set_stringn(
|
||||
1 + col * BASE_WIDTH,
|
||||
self.size.1 - col_height - 2 + row,
|
||||
&command,
|
||||
BASE_WIDTH as usize - 1,
|
||||
color,
|
||||
);
|
||||
row += 1;
|
||||
if row > col_height - 1 {
|
||||
row = 0;
|
||||
col += 1;
|
||||
}
|
||||
if col > max_col {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// render buffer text
|
||||
self.surface
|
||||
.set_string(1, self.size.1 - 1, &prompt.prompt, self.text_color);
|
||||
self.surface
|
||||
.set_string(2, self.size.1 - 1, &prompt.line, self.text_color);
|
||||
}
|
||||
|
||||
pub fn draw_and_swap(&mut self) {
|
||||
use tui::backend::Backend;
|
||||
// TODO: theres probably a better place for this
|
||||
self.terminal
|
||||
.backend_mut()
|
||||
.draw(self.cache.diff(&self.surface).into_iter());
|
||||
// swap the buffer
|
||||
std::mem::swap(&mut self.surface, &mut self.cache);
|
||||
self.surface.reset(); // reset is faster than allocating new empty surface
|
||||
}
|
||||
|
||||
pub fn render_cursor(&mut self, view: &View, prompt: Option<&Prompt>, viewport: Rect) {
|
||||
let mut stdout = stdout();
|
||||
match view.doc.mode() {
|
||||
Mode::Insert => write!(stdout, "\x1B[6 q"),
|
||||
mode => write!(stdout, "\x1B[2 q"),
|
||||
};
|
||||
let pos = if let Some(prompt) = prompt {
|
||||
Position::new(self.size.0 as usize, 2 + prompt.cursor)
|
||||
} else {
|
||||
let cursor = view.doc.state.selection().cursor();
|
||||
|
||||
let mut pos = view
|
||||
.screen_coords_at_pos(&view.doc.text().slice(..), cursor)
|
||||
.expect("Cursor is out of bounds.");
|
||||
pos.col += viewport.x as usize;
|
||||
pos.row += viewport.y as usize;
|
||||
pos
|
||||
};
|
||||
|
||||
execute!(stdout, cursor::MoveTo(pos.col as u16, pos.row as u16));
|
||||
}
|
||||
}
|
||||
|
||||
struct EditorView {
|
||||
keymap: Keymaps,
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
keymap: keymap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::compositor::Context;
|
||||
|
||||
impl Component for EditorView {
|
||||
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
match event {
|
||||
Event::Resize(width, height) => {
|
||||
// TODO: simplistic ensure cursor in view for now
|
||||
// TODO: loop over views
|
||||
if let Some(view) = cx.editor.view_mut() {
|
||||
view.size = (width, height);
|
||||
view.ensure_cursor_in_view()
|
||||
};
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(event) => {
|
||||
if let Some(view) = cx.editor.view_mut() {
|
||||
let keys = vec![event];
|
||||
// TODO: sequences (`gg`)
|
||||
let mode = view.doc.mode();
|
||||
// TODO: handle count other than 1
|
||||
let mut cx = commands::Context {
|
||||
view,
|
||||
executor: cx.executor,
|
||||
count: 1,
|
||||
callback: None,
|
||||
};
|
||||
|
||||
match mode {
|
||||
Mode::Insert => {
|
||||
if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
|
||||
command(&mut cx);
|
||||
} else if let KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
commands::insert::insert_char(&mut cx, c);
|
||||
}
|
||||
}
|
||||
mode => {
|
||||
if let Some(command) = self.keymap[&mode].get(&keys) {
|
||||
command(&mut cx);
|
||||
|
||||
// TODO: simplistic ensure cursor in view for now
|
||||
}
|
||||
}
|
||||
}
|
||||
// appease borrowck
|
||||
let callback = cx.callback.take();
|
||||
|
||||
view.ensure_cursor_in_view();
|
||||
|
||||
EventResult::Consumed(callback)
|
||||
} else {
|
||||
EventResult::Ignored
|
||||
}
|
||||
}
|
||||
Event::Mouse(_) => EventResult::Ignored,
|
||||
}
|
||||
}
|
||||
fn render(&mut self, renderer: &mut Renderer, cx: &mut Context) {
|
||||
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||
let viewport = Rect::new(OFFSET, 0, renderer.size.0, renderer.size.1 - 2); // - 2 for statusline and prompt
|
||||
|
||||
// SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
|
||||
// theme. Theme is immutable mutating view won't disrupt theme_ref.
|
||||
let theme_ref = unsafe { &*(&cx.editor.theme as *const Theme) };
|
||||
if let Some(view) = cx.editor.view_mut() {
|
||||
renderer.render_view(view, viewport, theme_ref);
|
||||
}
|
||||
|
||||
// TODO: drop unwrap
|
||||
renderer.render_cursor(cx.editor.view().unwrap(), None, viewport);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Application<'a> {
|
||||
pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
|
||||
let renderer = Renderer::new()?;
|
||||
let mut editor = Editor::new();
|
||||
let size = terminal.size()?;
|
||||
|
||||
if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() {
|
||||
editor.open(file, renderer.size)?;
|
||||
editor.open(file, (size.width, size.height))?;
|
||||
}
|
||||
|
||||
let mut compositor = Compositor::new();
|
||||
@ -466,7 +90,7 @@ impl<'a> Application<'a> {
|
||||
|
||||
let mut app = Self {
|
||||
editor,
|
||||
renderer,
|
||||
terminal,
|
||||
// TODO; move to state
|
||||
compositor,
|
||||
|
||||
@ -478,12 +102,17 @@ impl<'a> Application<'a> {
|
||||
}
|
||||
|
||||
fn render(&mut self) {
|
||||
let mut cx = crate::compositor::Context {
|
||||
editor: &mut self.editor,
|
||||
executor: &self.executor,
|
||||
};
|
||||
self.compositor.render(&mut self.renderer, &mut cx); // viewport,
|
||||
self.renderer.draw_and_swap();
|
||||
let executor = &self.executor;
|
||||
let editor = &mut self.editor;
|
||||
let compositor = &self.compositor;
|
||||
|
||||
// TODO: should be unnecessary
|
||||
// self.terminal.autoresize();
|
||||
let mut cx = crate::compositor::Context { editor, executor };
|
||||
let area = self.terminal.size().unwrap();
|
||||
compositor.render(area, self.terminal.current_buffer_mut(), &mut cx);
|
||||
|
||||
self.terminal.draw();
|
||||
}
|
||||
|
||||
pub async fn event_loop(&mut self) {
|
||||
@ -524,7 +153,7 @@ impl<'a> Application<'a> {
|
||||
// Handle key events
|
||||
let should_redraw = match event {
|
||||
Some(Ok(Event::Resize(width, height))) => {
|
||||
self.renderer.resize(width, height);
|
||||
self.terminal.resize(Rect::new(0, 0, width, height));
|
||||
|
||||
self.compositor
|
||||
.handle_event(Event::Resize(width, height), &mut cx)
|
||||
|
@ -13,10 +13,10 @@
|
||||
// Q: how does this work with popups?
|
||||
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
|
||||
|
||||
use crate::application::Renderer;
|
||||
use crossterm::event::Event;
|
||||
use smol::Executor;
|
||||
use tui::buffer::Buffer as Surface;
|
||||
use tui::layout::Rect;
|
||||
|
||||
pub type Callback = Box<dyn Fn(&mut Compositor)>;
|
||||
|
||||
@ -36,9 +36,9 @@ pub enum EventResult {
|
||||
|
||||
use helix_view::{Editor, View};
|
||||
// shared with commands.rs
|
||||
pub struct Context<'a, 'b> {
|
||||
pub struct Context<'a> {
|
||||
pub editor: &'a mut Editor,
|
||||
pub executor: &'a smol::Executor<'b>,
|
||||
pub executor: &'static smol::Executor<'static>,
|
||||
}
|
||||
|
||||
pub trait Component {
|
||||
@ -51,7 +51,7 @@ pub trait Component {
|
||||
true
|
||||
}
|
||||
|
||||
fn render(&mut self, renderer: &mut Renderer, ctx: &mut Context);
|
||||
fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
|
||||
}
|
||||
|
||||
// struct Editor { };
|
||||
@ -133,9 +133,9 @@ impl Compositor {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn render(&mut self, renderer: &mut Renderer, cx: &mut Context) {
|
||||
for layer in &mut self.layers {
|
||||
layer.render(renderer, cx)
|
||||
pub fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
for layer in &self.layers {
|
||||
layer.render(area, surface, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
311
helix-term/src/editor_view.rs
Normal file
311
helix-term/src/editor_view.rs
Normal file
@ -0,0 +1,311 @@
|
||||
use crate::application::text_color;
|
||||
use crate::commands;
|
||||
use crate::compositor::{Component, Compositor, EventResult};
|
||||
use crate::keymap::{self, Keymaps};
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{read, Event, EventStream, KeyCode, KeyEvent},
|
||||
};
|
||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||
use std::borrow::Cow;
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
use helix_core::{indent::TAB_WIDTH, syntax::HighlightEvent, Position, Range, State};
|
||||
|
||||
pub struct EditorView {
|
||||
keymap: Keymaps,
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
keymap: keymap::default(),
|
||||
}
|
||||
}
|
||||
pub fn render_view(
|
||||
&self,
|
||||
view: &mut View,
|
||||
viewport: Rect,
|
||||
surface: &mut Surface,
|
||||
theme: &Theme,
|
||||
) {
|
||||
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||
let area = Rect::new(OFFSET, 0, viewport.width - OFFSET, viewport.height - 2); // - 2 for statusline and prompt
|
||||
self.render_buffer(view, area, surface, theme);
|
||||
let area = Rect::new(0, viewport.height - 2, viewport.width, 1);
|
||||
self.render_statusline(view, viewport, surface, theme);
|
||||
}
|
||||
|
||||
// TODO: ideally not &mut View but highlights require it because of cursor cache
|
||||
pub fn render_buffer(
|
||||
&self,
|
||||
view: &mut View,
|
||||
viewport: Rect,
|
||||
surface: &mut Surface,
|
||||
theme: &Theme,
|
||||
) {
|
||||
// clear with background color
|
||||
surface.set_style(viewport, theme.get("ui.background"));
|
||||
|
||||
// TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
|
||||
let source_code = view.doc.text().to_string();
|
||||
|
||||
let last_line = view.last_line();
|
||||
|
||||
let range = {
|
||||
// calculate viewport byte ranges
|
||||
let start = view.doc.text().line_to_byte(view.first_line);
|
||||
let end = view.doc.text().line_to_byte(last_line)
|
||||
+ view.doc.text().line(last_line).len_bytes();
|
||||
|
||||
start..end
|
||||
};
|
||||
|
||||
// TODO: range doesn't actually restrict source, just highlight range
|
||||
// TODO: cache highlight results
|
||||
// TODO: only recalculate when state.doc is actually modified
|
||||
let highlights: Vec<_> = match view.doc.syntax.as_mut() {
|
||||
Some(syntax) => {
|
||||
syntax
|
||||
.highlight_iter(source_code.as_bytes(), Some(range), None, |_| None)
|
||||
.unwrap()
|
||||
.collect() // TODO: we collect here to avoid double borrow, fix later
|
||||
}
|
||||
None => vec![Ok(HighlightEvent::Source {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
})],
|
||||
};
|
||||
let mut spans = Vec::new();
|
||||
let mut visual_x = 0;
|
||||
let mut line = 0u16;
|
||||
let visible_selections: Vec<Range> = view
|
||||
.doc
|
||||
.state
|
||||
.selection()
|
||||
.ranges()
|
||||
.iter()
|
||||
// TODO: limit selection to one in viewport
|
||||
// .filter(|range| !range.is_empty()) // && range.overlaps(&Range::new(start, end + 1))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
'outer: for event in highlights {
|
||||
match event.unwrap() {
|
||||
HighlightEvent::HighlightStart(span) => {
|
||||
spans.push(span);
|
||||
}
|
||||
HighlightEvent::HighlightEnd => {
|
||||
spans.pop();
|
||||
}
|
||||
HighlightEvent::Source { start, end } => {
|
||||
// TODO: filter out spans out of viewport for now..
|
||||
|
||||
let start = view.doc.text().byte_to_char(start);
|
||||
let end = view.doc.text().byte_to_char(end); // <-- index 744, len 743
|
||||
|
||||
let text = view.doc.text().slice(start..end);
|
||||
|
||||
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
|
||||
|
||||
let style = match spans.first() {
|
||||
Some(span) => theme.get(theme.scopes()[span.0].as_str()),
|
||||
None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender
|
||||
};
|
||||
|
||||
// TODO: we could render the text to a surface, then cache that, that
|
||||
// way if only the selection/cursor changes we can copy from cache
|
||||
// and paint the new cursor.
|
||||
|
||||
let mut char_index = start;
|
||||
|
||||
// iterate over range char by char
|
||||
for grapheme in RopeGraphemes::new(&text) {
|
||||
// TODO: track current char_index
|
||||
|
||||
if grapheme == "\n" {
|
||||
visual_x = 0;
|
||||
line += 1;
|
||||
|
||||
// TODO: with proper iter this shouldn't be necessary
|
||||
if line >= viewport.height {
|
||||
break 'outer;
|
||||
}
|
||||
} else if grapheme == "\t" {
|
||||
visual_x += (TAB_WIDTH as u16);
|
||||
} else {
|
||||
// Cow will prevent allocations if span contained in a single slice
|
||||
// which should really be the majority case
|
||||
let grapheme = Cow::from(grapheme);
|
||||
let width = grapheme_width(&grapheme) as u16;
|
||||
|
||||
// TODO: this should really happen as an after pass
|
||||
let style = if visible_selections
|
||||
.iter()
|
||||
.any(|range| range.contains(char_index))
|
||||
{
|
||||
// cedar
|
||||
style.clone().bg(Color::Rgb(128, 47, 0))
|
||||
} else {
|
||||
style
|
||||
};
|
||||
|
||||
let style = if visible_selections
|
||||
.iter()
|
||||
.any(|range| range.head == char_index)
|
||||
{
|
||||
style.clone().bg(Color::Rgb(255, 255, 255))
|
||||
} else {
|
||||
style
|
||||
};
|
||||
|
||||
// ugh, improve with a traverse method
|
||||
// or interleave highlight spans with selection and diagnostic spans
|
||||
let style = if view.doc.diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index
|
||||
}) {
|
||||
style.clone().add_modifier(Modifier::UNDERLINED)
|
||||
} else {
|
||||
style
|
||||
};
|
||||
|
||||
// TODO: paint cursor heads except primary
|
||||
|
||||
surface.set_string(
|
||||
viewport.x + visual_x,
|
||||
viewport.y + line,
|
||||
grapheme,
|
||||
style,
|
||||
);
|
||||
|
||||
visual_x += width;
|
||||
}
|
||||
|
||||
char_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let style: Style = theme.get("ui.linenr");
|
||||
let warning: Style = theme.get("warning");
|
||||
let last_line = view.last_line();
|
||||
for (i, line) in (view.first_line..last_line).enumerate() {
|
||||
if view.doc.diagnostics.iter().any(|d| d.line == line) {
|
||||
surface.set_stringn(0, i as u16, "●", 1, warning);
|
||||
}
|
||||
|
||||
surface.set_stringn(1, i as u16, format!("{:>5}", line + 1), 5, style);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_statusline(
|
||||
&self,
|
||||
view: &View,
|
||||
viewport: Rect,
|
||||
surface: &mut Surface,
|
||||
theme: &Theme,
|
||||
) {
|
||||
let mode = match view.doc.mode() {
|
||||
Mode::Insert => "INS",
|
||||
Mode::Normal => "NOR",
|
||||
Mode::Goto => "GOTO",
|
||||
};
|
||||
// statusline
|
||||
surface.set_style(
|
||||
Rect::new(0, viewport.y, viewport.height, 1),
|
||||
theme.get("ui.statusline"),
|
||||
);
|
||||
surface.set_string(1, viewport.y, mode, text_color());
|
||||
|
||||
if let Some(path) = view.doc.path() {
|
||||
surface.set_string(6, viewport.y, path.to_string_lossy(), text_color());
|
||||
}
|
||||
|
||||
surface.set_string(
|
||||
viewport.width - 10,
|
||||
viewport.y,
|
||||
format!("{}", view.doc.diagnostics.len()),
|
||||
text_color(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
use crate::compositor::Context;
|
||||
|
||||
impl Component for EditorView {
|
||||
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
match event {
|
||||
Event::Resize(width, height) => {
|
||||
// TODO: simplistic ensure cursor in view for now
|
||||
// TODO: loop over views
|
||||
if let Some(view) = cx.editor.view_mut() {
|
||||
view.size = (width, height);
|
||||
view.ensure_cursor_in_view()
|
||||
};
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(event) => {
|
||||
if let Some(view) = cx.editor.view_mut() {
|
||||
let keys = vec![event];
|
||||
// TODO: sequences (`gg`)
|
||||
let mode = view.doc.mode();
|
||||
// TODO: handle count other than 1
|
||||
let mut cx = commands::Context {
|
||||
view,
|
||||
executor: cx.executor,
|
||||
count: 1,
|
||||
callback: None,
|
||||
};
|
||||
|
||||
match mode {
|
||||
Mode::Insert => {
|
||||
if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
|
||||
command(&mut cx);
|
||||
} else if let KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
commands::insert::insert_char(&mut cx, c);
|
||||
}
|
||||
}
|
||||
mode => {
|
||||
if let Some(command) = self.keymap[&mode].get(&keys) {
|
||||
command(&mut cx);
|
||||
|
||||
// TODO: simplistic ensure cursor in view for now
|
||||
}
|
||||
}
|
||||
}
|
||||
// appease borrowck
|
||||
let callback = cx.callback.take();
|
||||
|
||||
view.ensure_cursor_in_view();
|
||||
|
||||
EventResult::Consumed(callback)
|
||||
} else {
|
||||
EventResult::Ignored
|
||||
}
|
||||
}
|
||||
Event::Mouse(_) => EventResult::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
// SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
|
||||
// theme. Theme is immutable mutating view won't disrupt theme_ref.
|
||||
let theme_ref = unsafe { &*(&cx.editor.theme as *const Theme) };
|
||||
if let Some(view) = cx.editor.view_mut() {
|
||||
self.render_view(view, area, surface, theme_ref);
|
||||
}
|
||||
|
||||
// TODO: drop unwrap
|
||||
// TODO: !!! self.render_cursor(cx.editor.view().unwrap(), None, viewport);
|
||||
}
|
||||
}
|
@ -3,8 +3,10 @@
|
||||
mod application;
|
||||
mod commands;
|
||||
mod compositor;
|
||||
mod editor_view;
|
||||
mod keymap;
|
||||
mod prompt;
|
||||
mod terminal;
|
||||
|
||||
use application::Application;
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
use crate::{
|
||||
application::Renderer,
|
||||
compositor::{Component, Context, EventResult},
|
||||
};
|
||||
use crate::compositor::{Component, Context, EventResult};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use helix_view::Editor;
|
||||
use helix_view::Theme;
|
||||
use std::string::String;
|
||||
|
||||
pub struct Prompt {
|
||||
@ -85,6 +83,68 @@ impl Prompt {
|
||||
}
|
||||
}
|
||||
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
const BASE_WIDTH: u16 = 30;
|
||||
use crate::application::text_color;
|
||||
|
||||
impl Prompt {
|
||||
pub fn render_prompt(&self, area: Rect, surface: &mut Surface, theme: &Theme) {
|
||||
// completion
|
||||
if !self.completion.is_empty() {
|
||||
// TODO: find out better way of clearing individual lines of the screen
|
||||
let mut row = 0;
|
||||
let mut col = 0;
|
||||
let max_col = area.width / BASE_WIDTH;
|
||||
let col_height = ((self.completion.len() as u16 + max_col - 1) / max_col);
|
||||
|
||||
for i in (3..col_height + 3) {
|
||||
surface.set_string(
|
||||
0,
|
||||
area.height - i as u16,
|
||||
" ".repeat(area.width as usize),
|
||||
text_color(),
|
||||
);
|
||||
}
|
||||
surface.set_style(
|
||||
Rect::new(0, area.height - col_height - 2, area.width, col_height),
|
||||
theme.get("ui.statusline"),
|
||||
);
|
||||
for (i, command) in self.completion.iter().enumerate() {
|
||||
let color = if self.completion_selection_index.is_some()
|
||||
&& i == self.completion_selection_index.unwrap()
|
||||
{
|
||||
Style::default().bg(Color::Rgb(104, 060, 232))
|
||||
} else {
|
||||
text_color()
|
||||
};
|
||||
surface.set_stringn(
|
||||
1 + col * BASE_WIDTH,
|
||||
area.height - col_height - 2 + row,
|
||||
&command,
|
||||
BASE_WIDTH as usize - 1,
|
||||
color,
|
||||
);
|
||||
row += 1;
|
||||
if row > col_height - 1 {
|
||||
row = 0;
|
||||
col += 1;
|
||||
}
|
||||
if col > max_col {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// render buffer text
|
||||
surface.set_string(1, area.height - 1, &self.prompt, text_color());
|
||||
surface.set_string(2, area.height - 1, &self.line, text_color());
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Prompt {
|
||||
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
let event = match event {
|
||||
@ -137,7 +197,7 @@ impl Component for Prompt {
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
|
||||
fn render(&mut self, renderer: &mut Renderer, cx: &mut Context) {
|
||||
renderer.render_prompt(self, &cx.editor.theme)
|
||||
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
self.render_prompt(area, surface, &cx.editor.theme)
|
||||
}
|
||||
}
|
||||
|
221
helix-term/src/terminal.rs
Normal file
221
helix-term/src/terminal.rs
Normal file
@ -0,0 +1,221 @@
|
||||
use std::io;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// UNSTABLE
|
||||
enum ResizeBehavior {
|
||||
Fixed,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// UNSTABLE
|
||||
pub struct Viewport {
|
||||
area: Rect,
|
||||
resize_behavior: ResizeBehavior,
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
/// UNSTABLE
|
||||
pub fn fixed(area: Rect) -> Viewport {
|
||||
Viewport {
|
||||
area,
|
||||
resize_behavior: ResizeBehavior::Fixed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
#[derive(Debug)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
backend: B,
|
||||
/// Holds the results of the current and previous draw calls. The two are compared at the end
|
||||
/// of each draw pass to output the necessary updates to the terminal
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the current buffer in the previous array
|
||||
current: usize,
|
||||
/// Whether the cursor is currently hidden
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
eprintln!("Failed to show the cursor: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
let size = backend.size()?;
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport {
|
||||
area: size,
|
||||
resize_behavior: ResizeBehavior::Auto,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// UNSTABLE
|
||||
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [
|
||||
Buffer::empty(options.viewport.area),
|
||||
Buffer::empty(options.viewport.area),
|
||||
],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
})
|
||||
}
|
||||
|
||||
// /// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
// pub fn get_frame(&mut self) -> Frame<B> {
|
||||
// Frame {
|
||||
// terminal: self,
|
||||
// cursor_position: None,
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
pub fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
pub fn backend_mut(&mut self) -> &mut B {
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
||||
/// be saved so the size can remain consistent when rendering.
|
||||
/// This leads to a full clear of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.viewport.area = area;
|
||||
self.clear()
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
if self.viewport.resize_behavior == ResizeBehavior::Auto {
|
||||
let size = self.size()?;
|
||||
if size != self.viewport.area {
|
||||
self.resize(size)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
pub fn draw(&mut self) -> io::Result<()> {
|
||||
// // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// // and the terminal (if growing), which may OOB.
|
||||
// self.autoresize()?;
|
||||
|
||||
// let mut frame = self.get_frame();
|
||||
// f(&mut frame);
|
||||
// // We can't change the cursor position right away because we have to flush the frame to
|
||||
// // stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// // Terminal. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
// let cursor_position = frame.cursor_position;
|
||||
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
// match cursor_position {
|
||||
// None => self.hide_cursor()?,
|
||||
// Some((x, y)) => {
|
||||
// self.show_cursor()?;
|
||||
// self.set_cursor(x, y)?;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Swap buffers
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
self.backend.clear()?;
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user