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, } ///Helper types so I don't mix these two up struct CellWidth(f32); struct LineHeight(f32); #[derive(Clone, Debug, Default)] struct LayoutCell { point: Point, text: Line, background_color: Color, } impl LayoutCell { fn new(point: Point, 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, Line)>, background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan line_height: LineHeight, em_width: CellWidth, cursor: Option, background_color: Color, cur_size: SizeInfo, } impl TerminalEl { pub fn new(view: WeakViewHandle) -> 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::()); 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::()).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::, 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::>(); 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, text_style: &TextStyle, terminal_theme: &TerminalStyle, text_layout_cache: &TextLayoutCache, ) -> Vec { 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::>() }) .flatten() .collect::>() } // 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); } } }