Files
oak-gpui/crates/terminal/src/terminal_element.rs
T
2022-07-05 17:10:01 -07:00

569 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use alacritty_terminal::{
ansi::Color as AnsiColor,
grid::{Dimensions, GridIterator, Indexed},
index::Point,
term::{
cell::{Cell, Flags},
SizeInfo,
},
};
use editor::{Cursor, CursorShape};
use gpui::{
color::Color,
elements::*,
fonts::{TextStyle, Underline},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::json,
text_layout::{Line, RunStyle},
Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache,
WeakViewHandle,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use settings::Settings;
use std::rc::Rc;
use theme::TerminalStyle;
use crate::{gpui_func_tools::paint_layer, Input, ScrollTerminal, Terminal};
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.
const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
///Used to display the grid as passed to Alacritty and the TTY.
///Useful for debugging inconsistencies between behavior and display
#[cfg(debug_assertions)]
const DEBUG_GRID: bool = false;
///The GPUI element that paints the terminal.
pub struct TerminalEl {
view: WeakViewHandle<Terminal>,
}
///Helper types so I don't mix these two up
struct CellWidth(f32);
struct LineHeight(f32);
#[derive(Clone, Debug, Default)]
struct LayoutCell {
point: Point<i32, i32>,
text: Line,
background_color: Color,
}
impl LayoutCell {
fn new(point: Point<i32, i32>, text: Line, background_color: Color) -> LayoutCell {
LayoutCell {
point,
text,
background_color,
}
}
}
///The information generated during layout that is nescessary for painting
pub struct LayoutState {
cells: Vec<(Point<i32, i32>, Line)>,
background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan
line_height: LineHeight,
em_width: CellWidth,
cursor: Option<Cursor>,
background_color: Color,
cur_size: SizeInfo,
}
impl TerminalEl {
pub fn new(view: WeakViewHandle<Terminal>) -> TerminalEl {
TerminalEl { view }
}
}
impl Element for TerminalEl {
type LayoutState = LayoutState;
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
cx: &mut gpui::LayoutContext,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
//Settings immutably borrows cx here for the settings and font cache
//and we need to modify the cx to resize the terminal. So instead of
//storing Settings or the font_cache(), we toss them ASAP and then reborrow later
let text_style = make_text_style(cx.font_cache(), cx.global::<Settings>());
let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size));
let cell_width = CellWidth(
cx.font_cache()
.em_advance(text_style.font_id, text_style.font_size),
);
let view_handle = self.view.upgrade(cx).unwrap();
//Tell the view our new size. Requires a mutable borrow of cx and the view
let cur_size = make_new_size(constraint, &cell_width, &line_height);
//Note that set_size locks and mutates the terminal.
//TODO: Would be nice to lock once for the whole of layout
view_handle.update(cx.app, |view, _cx| view.set_size(cur_size));
//Now that we're done with the mutable portion, grab the immutable settings and view again
let terminal_theme = &(cx.global::<Settings>()).theme.terminal;
let term = view_handle.read(cx).term.lock();
let grid = term.grid();
let cursor_point = grid.cursor.point;
let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
let content = term.renderable_content();
let layout_cells = layout_cells(
content.display_iter,
&text_style,
terminal_theme,
cx.text_layout_cache,
);
let cells = layout_cells
.iter()
.map(|c| (c.point, c.text.clone()))
.collect::<Vec<(Point<i32, i32>, Line)>>();
let background_rects = layout_cells
.iter()
.map(|cell| {
(
RectF::new(
vec2f(
cell.point.column as f32 * cell_width.0,
cell.point.line as f32 * line_height.0,
),
vec2f(cell_width.0, line_height.0),
),
cell.background_color,
)
})
.collect::<Vec<(RectF, Color)>>();
let block_text = cx.text_layout_cache.layout_str(
&cursor_text,
text_style.font_size,
&[(
cursor_text.len(),
RunStyle {
font_id: text_style.font_id,
color: terminal_theme.background,
underline: Default::default(),
},
)],
);
let cursor = get_cursor_shape(
content.cursor.point.line.0 as usize,
content.cursor.point.column.0 as usize,
content.display_offset,
&line_height,
&cell_width,
cur_size.total_lines(),
&block_text,
)
.map(move |(cursor_position, block_width)| {
let block_width = if block_width != 0.0 {
block_width
} else {
cell_width.0
};
Cursor::new(
cursor_position,
block_width,
line_height.0,
terminal_theme.cursor,
CursorShape::Block,
Some(block_text.clone()),
)
});
(
constraint.max,
LayoutState {
cells,
line_height,
em_width: cell_width,
cursor,
cur_size,
background_rects,
background_color: terminal_theme.background,
},
)
}
fn paint(
&mut self,
bounds: gpui::geometry::rect::RectF,
visible_bounds: gpui::geometry::rect::RectF,
layout: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
//Setup element stuff
let clip_bounds = Some(visible_bounds);
paint_layer(cx, clip_bounds, |cx| {
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
cx.scene.push_mouse_region(MouseRegion {
view_id: self.view.id(),
mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())),
bounds: visible_bounds,
..Default::default()
});
let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
paint_layer(cx, clip_bounds, |cx| {
//Start with a background color
cx.scene.push_quad(Quad {
bounds: RectF::new(bounds.origin(), bounds.size()),
background: Some(layout.background_color),
border: Default::default(),
corner_radius: 0.,
});
//Draw cell backgrounds
for background_rect in &layout.background_rects {
let new_origin = origin + background_rect.0.origin();
cx.scene.push_quad(Quad {
bounds: RectF::new(new_origin, background_rect.0.size()),
background: Some(background_rect.1),
border: Default::default(),
corner_radius: 0.,
})
}
});
//Draw text
paint_layer(cx, clip_bounds, |cx| {
for (point, cell) in &layout.cells {
let cell_origin = vec2f(
origin.x() + point.column as f32 * layout.em_width.0,
origin.y() + point.line as f32 * layout.line_height.0,
);
cell.paint(cell_origin, visible_bounds, layout.line_height.0, cx);
}
});
//Draw cursor
if let Some(cursor) = &layout.cursor {
paint_layer(cx, clip_bounds, |cx| {
cursor.paint(origin, cx);
})
}
#[cfg(debug_assertions)]
if DEBUG_GRID {
paint_layer(cx, clip_bounds, |cx| {
draw_debug_grid(bounds, layout, cx);
});
}
});
}
fn dispatch_event(
&mut self,
event: &gpui::Event,
_bounds: gpui::geometry::rect::RectF,
visible_bounds: gpui::geometry::rect::RectF,
layout: &mut Self::LayoutState,
_paint: &mut Self::PaintState,
cx: &mut gpui::EventContext,
) -> bool {
match event {
Event::ScrollWheel {
delta, position, ..
} => visible_bounds
.contains_point(*position)
.then(|| {
let vertical_scroll =
(delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
})
.is_some(),
Event::KeyDown {
input: Some(input), ..
} => cx
.is_parent_view_focused()
.then(|| {
cx.dispatch_action(Input(input.to_string()));
})
.is_some(),
_ => false,
}
}
fn debug(
&self,
_bounds: gpui::geometry::rect::RectF,
_layout: &Self::LayoutState,
_paint: &Self::PaintState,
_cx: &gpui::DebugContext,
) -> gpui::serde_json::Value {
json!({
"type": "TerminalElement",
})
}
}
///Configures a text style from the current settings.
fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
TextStyle {
color: settings.theme.editor.text_color,
font_family_id: settings.buffer_font_family,
font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
font_id: font_cache
.select_font(settings.buffer_font_family, &Default::default())
.unwrap(),
font_size: settings.buffer_font_size,
font_properties: Default::default(),
underline: Default::default(),
}
}
///Configures a size info object from the given information.
fn make_new_size(
constraint: SizeConstraint,
cell_width: &CellWidth,
line_height: &LineHeight,
) -> SizeInfo {
SizeInfo::new(
constraint.max.x() - cell_width.0,
constraint.max.y(),
cell_width.0,
line_height.0,
0.,
0.,
false,
)
}
fn layout_cells(
grid: GridIterator<Cell>,
text_style: &TextStyle,
terminal_theme: &TerminalStyle,
text_layout_cache: &TextLayoutCache,
) -> Vec<LayoutCell> {
let mut line_count: i32 = 0;
let lines = grid.group_by(|i| i.point.line);
lines
.into_iter()
.map(|(_, line)| {
line_count += 1;
line.map(|indexed_cell| {
let cell_text = &indexed_cell.c.to_string();
let cell_style = cell_style(&indexed_cell, terminal_theme, text_style);
let layout_cell = text_layout_cache.layout_str(
cell_text,
text_style.font_size,
&[(cell_text.len(), cell_style)],
);
LayoutCell::new(
Point::new(line_count - 1, indexed_cell.point.column.0 as i32),
layout_cell,
convert_color(&indexed_cell.bg, terminal_theme),
)
})
.collect::<Vec<LayoutCell>>()
})
.flatten()
.collect::<Vec<LayoutCell>>()
}
// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
// the same position for sequential indexes. Use em_width instead
//TODO: This function is messy, too many arguments and too many ifs. Simplify.
fn get_cursor_shape(
line: usize,
line_index: usize,
display_offset: usize,
line_height: &LineHeight,
cell_width: &CellWidth,
total_lines: usize,
text_fragment: &Line,
) -> Option<(Vector2F, f32)> {
let cursor_line = line + display_offset;
if cursor_line <= total_lines {
let cursor_width = if text_fragment.width() == 0. {
cell_width.0
} else {
text_fragment.width()
};
Some((
vec2f(
line_index as f32 * cell_width.0,
cursor_line as f32 * line_height.0,
),
cursor_width,
))
} else {
None
}
}
///Convert the Alacritty cell styles to GPUI text styles and background color
fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle, text_style: &TextStyle) -> RunStyle {
let flags = indexed.cell.flags;
let fg = convert_color(&indexed.cell.fg, style);
let underline = flags
.contains(Flags::UNDERLINE)
.then(|| Underline {
color: Some(fg),
squiggly: false,
thickness: OrderedFloat(1.),
})
.unwrap_or_default();
RunStyle {
color: fg,
font_id: text_style.font_id,
underline,
}
}
///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
match alac_color {
//Named and theme defined colors
alacritty_terminal::ansi::Color::Named(n) => match n {
alacritty_terminal::ansi::NamedColor::Black => style.black,
alacritty_terminal::ansi::NamedColor::Red => style.red,
alacritty_terminal::ansi::NamedColor::Green => style.green,
alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
alacritty_terminal::ansi::NamedColor::Blue => style.blue,
alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
alacritty_terminal::ansi::NamedColor::White => style.white,
alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
alacritty_terminal::ansi::NamedColor::Background => style.background,
alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
},
//'True' colors
alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
//8 bit, indexed colors
alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style),
}
}
///Converts an 8 bit ANSI color to it's GPUI equivalent.
pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color {
match index {
//0-15 are the same as the named colors above
0 => style.black,
1 => style.red,
2 => style.green,
3 => style.yellow,
4 => style.blue,
5 => style.magenta,
6 => style.cyan,
7 => style.white,
8 => style.bright_black,
9 => style.bright_red,
10 => style.bright_green,
11 => style.bright_yellow,
12 => style.bright_blue,
13 => style.bright_magenta,
14 => style.bright_cyan,
15 => style.bright_white,
//16-231 are mapped to their RGB colors on a 0-5 range per channel
16..=231 => {
let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components
let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
}
//232-255 are a 24 step grayscale from black to white
232..=255 => {
let i = index - 232; //Align index to 0..24
let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
}
}
}
///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
///
///Wikipedia gives a formula for calculating the index for a given color:
///
///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
///
///This function does the reverse, calculating the r, g, and b components from a given index.
fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
debug_assert!(i >= &16 && i <= &231);
let i = i - 16;
let r = (i - (i % 36)) / 36;
let g = ((i % 36) - (i % 6)) / 6;
let b = (i % 36) % 6;
(r, g, b)
}
///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
///Display and conceptual grid.
#[cfg(debug_assertions)]
fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
let width = layout.cur_size.width();
let height = layout.cur_size.height();
//Alacritty uses 'as usize', so shall we.
for col in 0..(width / layout.em_width.0).round() as usize {
cx.scene.push_quad(Quad {
bounds: RectF::new(
bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
vec2f(1., height),
),
background: Some(Color::green()),
border: Default::default(),
corner_radius: 0.,
});
}
for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
cx.scene.push_quad(Quad {
bounds: RectF::new(
bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
vec2f(width, 1.),
),
background: Some(Color::green()),
border: Default::default(),
corner_radius: 0.,
});
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_rgb_for_index() {
//Test every possible value in the color cube
for i in 16..=231 {
let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8));
assert_eq!(i, 16 + 36 * r + 6 * g + b);
}
}
}