Initial commit: VT220 emulator with multi-window support
This commit is contained in:
504
src/main.rs
Normal file
504
src/main.rs
Normal file
@@ -0,0 +1,504 @@
|
||||
mod renderer;
|
||||
mod terminal;
|
||||
|
||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||
use renderer::{CellColor, CursorInfo, TerminalLine, TerminalRenderer, TextSpan};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use terminal::TerminalEmulator;
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
dpi::LogicalSize,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoop},
|
||||
keyboard::{Key, NamedKey, ModifiersState},
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let mut app = App {
|
||||
windows: HashMap::new(),
|
||||
next_window_offset: (50, 50),
|
||||
};
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
}
|
||||
|
||||
struct App {
|
||||
windows: HashMap<WindowId, WindowState>,
|
||||
next_window_offset: (i32, i32),
|
||||
}
|
||||
|
||||
struct WindowState {
|
||||
window: Arc<Window>,
|
||||
renderer: TerminalRenderer,
|
||||
terminal: TerminalEmulator,
|
||||
clipboard: Option<ClipboardContext>,
|
||||
columns: usize,
|
||||
lines: usize,
|
||||
needs_redraw: bool,
|
||||
modifiers: ModifiersState,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn create_window(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let (ox, oy) = self.next_window_offset;
|
||||
self.next_window_offset = ((ox + 30) % 300, (oy + 30) % 300);
|
||||
|
||||
let window_attributes = Window::default_attributes()
|
||||
.with_inner_size(LogicalSize::new(800.0, 600.0))
|
||||
.with_title("VT220 Emulator")
|
||||
.with_position(winit::dpi::PhysicalPosition::new(ox, oy));
|
||||
|
||||
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
|
||||
let renderer = pollster::block_on(TerminalRenderer::new(window.clone(), event_loop));
|
||||
|
||||
let inner = window.inner_size();
|
||||
let cols = (inner.width as f32 / renderer.cell_width).max(2.0) as usize;
|
||||
let lines = (inner.height as f32 / renderer.cell_height).max(1.0) as usize;
|
||||
|
||||
let terminal = TerminalEmulator::new(cols, lines).expect("Failed to create terminal");
|
||||
let clipboard = ClipboardContext::new().ok();
|
||||
|
||||
let window_id = window.id();
|
||||
let state = WindowState {
|
||||
window,
|
||||
renderer,
|
||||
terminal,
|
||||
clipboard,
|
||||
columns: cols,
|
||||
lines,
|
||||
needs_redraw: true,
|
||||
modifiers: ModifiersState::empty(),
|
||||
};
|
||||
|
||||
self.windows.insert(window_id, state);
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.windows.is_empty() {
|
||||
self.create_window(event_loop);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
// Handle window destruction events first to avoid borrow issues.
|
||||
match event {
|
||||
WindowEvent::CloseRequested | WindowEvent::Destroyed => {
|
||||
self.windows.remove(&window_id);
|
||||
if self.windows.is_empty() {
|
||||
event_loop.exit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let Some(state) = self.windows.get_mut(&window_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
WindowEvent::Resized(size) => {
|
||||
state.renderer.resize(size);
|
||||
let cols = (size.width as f32 / state.renderer.cell_width).max(2.0) as usize;
|
||||
let lines = (size.height as f32 / state.renderer.cell_height).max(1.0) as usize;
|
||||
state.columns = cols;
|
||||
state.lines = lines;
|
||||
state.terminal.resize(
|
||||
cols,
|
||||
lines,
|
||||
state.renderer.cell_width as u16,
|
||||
state.renderer.cell_height as u16,
|
||||
);
|
||||
state.window.request_redraw();
|
||||
}
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
let logical_key = event.logical_key;
|
||||
let mods = state.modifiers;
|
||||
|
||||
// New window shortcut.
|
||||
if mods.control_key() && mods.shift_key() {
|
||||
if let Key::Character(c) = &logical_key {
|
||||
if c.as_str().eq_ignore_ascii_case("n") {
|
||||
self.create_window(event_loop);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy / Paste shortcuts.
|
||||
if mods.control_key() && mods.shift_key() {
|
||||
if let Key::Character(c) = &logical_key {
|
||||
let ch = c.as_str();
|
||||
if ch.eq_ignore_ascii_case("c") {
|
||||
let clipboard = &mut state.clipboard;
|
||||
state.terminal.with_term(|term| {
|
||||
if let Some(text) = term.selection_to_string() {
|
||||
if let Some(cb) = clipboard {
|
||||
let _ = cb.set_contents(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ch.eq_ignore_ascii_case("v") {
|
||||
if let Some(cb) = &mut state.clipboard {
|
||||
if let Ok(text) = cb.get_contents() {
|
||||
state.terminal.send_input(text.as_bytes());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut input_bytes: Vec<u8> = Vec::new();
|
||||
|
||||
match logical_key {
|
||||
Key::Named(NamedKey::Enter) => {
|
||||
input_bytes.push(b'\r');
|
||||
}
|
||||
Key::Named(NamedKey::Backspace) => {
|
||||
input_bytes.push(0x7f);
|
||||
}
|
||||
Key::Named(NamedKey::Tab) => {
|
||||
input_bytes.push(b'\t');
|
||||
}
|
||||
Key::Named(NamedKey::Escape) => {
|
||||
input_bytes.push(0x1b);
|
||||
}
|
||||
Key::Named(NamedKey::ArrowUp) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[A");
|
||||
}
|
||||
Key::Named(NamedKey::ArrowDown) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[B");
|
||||
}
|
||||
Key::Named(NamedKey::ArrowRight) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[C");
|
||||
}
|
||||
Key::Named(NamedKey::ArrowLeft) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[D");
|
||||
}
|
||||
Key::Named(NamedKey::Home) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[H");
|
||||
}
|
||||
Key::Named(NamedKey::End) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[F");
|
||||
}
|
||||
Key::Named(NamedKey::Delete) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[3~");
|
||||
}
|
||||
Key::Named(NamedKey::Insert) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[2~");
|
||||
}
|
||||
Key::Named(NamedKey::PageUp) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[5~");
|
||||
}
|
||||
Key::Named(NamedKey::PageDown) => {
|
||||
input_bytes.extend_from_slice(b"\x1b[6~");
|
||||
}
|
||||
Key::Named(NamedKey::F1) => {
|
||||
input_bytes.extend_from_slice(b"\x1bOP");
|
||||
}
|
||||
Key::Named(NamedKey::F2) => {
|
||||
input_bytes.extend_from_slice(b"\x1bOQ");
|
||||
}
|
||||
Key::Named(NamedKey::F3) => {
|
||||
input_bytes.extend_from_slice(b"\x1bOR");
|
||||
}
|
||||
Key::Named(NamedKey::F4) => {
|
||||
input_bytes.extend_from_slice(b"\x1bOS");
|
||||
}
|
||||
Key::Character(c) => {
|
||||
let s = c.as_str();
|
||||
if s.len() == 1 {
|
||||
let b = s.as_bytes()[0];
|
||||
input_bytes.push(b);
|
||||
} else {
|
||||
input_bytes.extend_from_slice(s.as_bytes());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if !input_bytes.is_empty() {
|
||||
state.terminal.send_input(&input_bytes);
|
||||
state.window.request_redraw();
|
||||
}
|
||||
}
|
||||
WindowEvent::ModifiersChanged(mods) => {
|
||||
state.modifiers = mods.state();
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
let lines = match delta {
|
||||
winit::event::MouseScrollDelta::LineDelta(_, y) => -y as i32,
|
||||
winit::event::MouseScrollDelta::PixelDelta(pos) => {
|
||||
-(pos.y / state.renderer.cell_height as f64) as i32
|
||||
}
|
||||
};
|
||||
if lines != 0 {
|
||||
state.terminal.with_term(|term| {
|
||||
term.scroll_display(alacritty_terminal::grid::Scroll::Delta(lines));
|
||||
});
|
||||
state.window.request_redraw();
|
||||
}
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
state.needs_redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if state.needs_redraw {
|
||||
state.needs_redraw = false;
|
||||
draw(state);
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
let mut windows_to_redraw = Vec::new();
|
||||
|
||||
for (window_id, state) in &mut self.windows {
|
||||
let events = state.terminal.read_events();
|
||||
let mut need_redraw = false;
|
||||
for event in events {
|
||||
match event {
|
||||
alacritty_terminal::event::Event::Wakeup => {
|
||||
need_redraw = true;
|
||||
}
|
||||
alacritty_terminal::event::Event::ClipboardStore(_ty, text) => {
|
||||
if let Some(cb) = &mut state.clipboard {
|
||||
let _ = cb.set_contents(text);
|
||||
}
|
||||
}
|
||||
alacritty_terminal::event::Event::ClipboardLoad(_ty, formatter) => {
|
||||
if let Some(cb) = &mut state.clipboard {
|
||||
if let Ok(content) = cb.get_contents() {
|
||||
let formatted = formatter(&content);
|
||||
state.terminal.send_input(formatted.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
alacritty_terminal::event::Event::Title(title) => {
|
||||
state.window.set_title(&format!("VT220 Emulator - {}", title));
|
||||
}
|
||||
alacritty_terminal::event::Event::Exit => {
|
||||
windows_to_redraw.push(*window_id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if need_redraw {
|
||||
state.window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
for window_id in windows_to_redraw {
|
||||
self.windows.remove(&window_id);
|
||||
}
|
||||
if self.windows.is_empty() {
|
||||
_event_loop.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(state: &mut WindowState) {
|
||||
let mut lines: Vec<TerminalLine> = Vec::with_capacity(state.lines);
|
||||
let mut cursor: Option<CursorInfo> = None;
|
||||
|
||||
state.terminal.with_term(|term| {
|
||||
let grid = term.grid();
|
||||
let colors = term.colors();
|
||||
let mode = *term.mode();
|
||||
let display_offset = grid.display_offset() as i32;
|
||||
|
||||
let mut current_line_spans: Vec<TextSpan> = Vec::new();
|
||||
let mut current_text = String::new();
|
||||
let mut current_fg = CellColor::new(255, 255, 255);
|
||||
let mut last_line = 0i32;
|
||||
|
||||
let cursor_point = grid.cursor.point;
|
||||
let cursor_viewport_y = cursor_point.line.0 + display_offset;
|
||||
let cursor_info = CursorInfo {
|
||||
x: cursor_point.column.0,
|
||||
y: cursor_viewport_y as usize,
|
||||
visible: mode.contains(alacritty_terminal::term::TermMode::SHOW_CURSOR),
|
||||
};
|
||||
cursor = Some(cursor_info);
|
||||
|
||||
for indexed in grid.display_iter() {
|
||||
let cell = indexed.cell;
|
||||
let point = indexed.point;
|
||||
let line_in_viewport = point.line.0 + display_offset;
|
||||
|
||||
if line_in_viewport != last_line {
|
||||
if !current_text.is_empty() {
|
||||
current_line_spans.push(TextSpan {
|
||||
text: current_text.clone(),
|
||||
fg: current_fg,
|
||||
});
|
||||
}
|
||||
lines.push(TerminalLine {
|
||||
spans: std::mem::take(&mut current_line_spans),
|
||||
});
|
||||
current_text.clear();
|
||||
last_line = line_in_viewport;
|
||||
}
|
||||
|
||||
let fg = resolve_color(cell.fg, colors, true);
|
||||
let bg = resolve_color(cell.bg, colors, false);
|
||||
|
||||
let is_cursor = cursor_info.visible
|
||||
&& cursor_info.x == point.column.0
|
||||
&& cursor_info.y == line_in_viewport as usize;
|
||||
|
||||
if is_cursor {
|
||||
if !current_text.is_empty() {
|
||||
current_line_spans.push(TextSpan {
|
||||
text: current_text.clone(),
|
||||
fg: current_fg,
|
||||
});
|
||||
current_text.clear();
|
||||
}
|
||||
let ch = if cell.c == '\0' { ' ' } else { cell.c };
|
||||
current_line_spans.push(TextSpan {
|
||||
text: ch.to_string(),
|
||||
fg: bg,
|
||||
});
|
||||
current_fg = fg;
|
||||
} else {
|
||||
if fg != current_fg && !current_text.is_empty() {
|
||||
current_line_spans.push(TextSpan {
|
||||
text: current_text.clone(),
|
||||
fg: current_fg,
|
||||
});
|
||||
current_text.clear();
|
||||
}
|
||||
current_fg = fg;
|
||||
let ch = if cell.c == '\0' { ' ' } else { cell.c };
|
||||
current_text.push(ch);
|
||||
if let Some(zw) = cell.zerowidth() {
|
||||
for &z in zw {
|
||||
current_text.push(z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !current_text.is_empty() {
|
||||
current_line_spans.push(TextSpan {
|
||||
text: current_text,
|
||||
fg: current_fg,
|
||||
});
|
||||
}
|
||||
if !current_line_spans.is_empty() {
|
||||
lines.push(TerminalLine {
|
||||
spans: current_line_spans,
|
||||
});
|
||||
}
|
||||
|
||||
while lines.len() < state.lines {
|
||||
lines.push(TerminalLine { spans: vec![] });
|
||||
}
|
||||
});
|
||||
|
||||
state.renderer.render(&lines, cursor);
|
||||
}
|
||||
|
||||
fn resolve_color(
|
||||
color: alacritty_terminal::vte::ansi::Color,
|
||||
colors: &alacritty_terminal::term::color::Colors,
|
||||
is_fg: bool,
|
||||
) -> CellColor {
|
||||
use alacritty_terminal::vte::ansi::Color;
|
||||
|
||||
let rgb = match color {
|
||||
Color::Named(named) => {
|
||||
if let Some(rgb) = colors[named] {
|
||||
rgb
|
||||
} else {
|
||||
return named_color_default(named, is_fg);
|
||||
}
|
||||
}
|
||||
Color::Spec(rgb) => rgb,
|
||||
Color::Indexed(idx) => {
|
||||
if let Some(rgb) = colors[idx as usize] {
|
||||
rgb
|
||||
} else {
|
||||
return indexed_color_default(idx);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CellColor::new(rgb.r, rgb.g, rgb.b)
|
||||
}
|
||||
|
||||
fn named_color_default(
|
||||
named: alacritty_terminal::vte::ansi::NamedColor,
|
||||
is_fg: bool,
|
||||
) -> CellColor {
|
||||
use alacritty_terminal::vte::ansi::NamedColor;
|
||||
match named {
|
||||
NamedColor::Black => CellColor::new(0, 0, 0),
|
||||
NamedColor::Red => CellColor::new(205, 0, 0),
|
||||
NamedColor::Green => CellColor::new(0, 205, 0),
|
||||
NamedColor::Yellow => CellColor::new(205, 205, 0),
|
||||
NamedColor::Blue => CellColor::new(0, 0, 238),
|
||||
NamedColor::Magenta => CellColor::new(205, 0, 205),
|
||||
NamedColor::Cyan => CellColor::new(0, 205, 205),
|
||||
NamedColor::White => CellColor::new(229, 229, 229),
|
||||
NamedColor::BrightBlack => CellColor::new(127, 127, 127),
|
||||
NamedColor::BrightRed => CellColor::new(255, 0, 0),
|
||||
NamedColor::BrightGreen => CellColor::new(0, 255, 0),
|
||||
NamedColor::BrightYellow => CellColor::new(255, 255, 0),
|
||||
NamedColor::BrightBlue => CellColor::new(92, 92, 255),
|
||||
NamedColor::BrightMagenta => CellColor::new(255, 0, 255),
|
||||
NamedColor::BrightCyan => CellColor::new(0, 255, 255),
|
||||
NamedColor::BrightWhite => CellColor::new(255, 255, 255),
|
||||
NamedColor::Foreground => {
|
||||
if is_fg {
|
||||
CellColor::new(229, 229, 229)
|
||||
} else {
|
||||
CellColor::new(0, 0, 0)
|
||||
}
|
||||
}
|
||||
NamedColor::Background => {
|
||||
if is_fg {
|
||||
CellColor::new(229, 229, 229)
|
||||
} else {
|
||||
CellColor::new(0, 0, 0)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if is_fg {
|
||||
CellColor::new(229, 229, 229)
|
||||
} else {
|
||||
CellColor::new(0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn indexed_color_default(idx: u8) -> CellColor {
|
||||
if idx < 16 {
|
||||
return CellColor::new(128, 128, 128);
|
||||
} else if idx < 232 {
|
||||
let i = idx - 16;
|
||||
let r = (i / 36) * 51;
|
||||
let g = ((i % 36) / 6) * 51;
|
||||
let b = (i % 6) * 51;
|
||||
CellColor::new(r, g, b)
|
||||
} else {
|
||||
let gray = (idx - 232) * 10 + 8;
|
||||
CellColor::new(gray, gray, gray)
|
||||
}
|
||||
}
|
||||
305
src/renderer.rs
Normal file
305
src/renderer.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
use glyphon::{
|
||||
Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
|
||||
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use wgpu::{
|
||||
CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Instance,
|
||||
InstanceDescriptor, LoadOp, MultisampleState, Operations, PresentMode, Queue,
|
||||
RenderPassColorAttachment, RenderPassDescriptor, RequestAdapterOptions, Surface,
|
||||
SurfaceConfiguration, TextureFormat, TextureUsages, TextureViewDescriptor,
|
||||
};
|
||||
use winit::{
|
||||
dpi::PhysicalSize,
|
||||
event_loop::ActiveEventLoop,
|
||||
window::Window,
|
||||
};
|
||||
|
||||
pub const FONT_SIZE: f32 = 14.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct CellColor {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
}
|
||||
|
||||
impl CellColor {
|
||||
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||
Self { r, g, b }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextSpan {
|
||||
pub text: String,
|
||||
pub fg: CellColor,
|
||||
}
|
||||
|
||||
pub struct TerminalLine {
|
||||
pub spans: Vec<TextSpan>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CursorInfo {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
pub struct TerminalRenderer {
|
||||
pub device: Device,
|
||||
pub queue: Queue,
|
||||
pub surface: Surface<'static>,
|
||||
pub surface_config: SurfaceConfiguration,
|
||||
pub instance: Instance,
|
||||
|
||||
pub font_system: FontSystem,
|
||||
pub swash_cache: SwashCache,
|
||||
pub viewport: Viewport,
|
||||
pub atlas: TextAtlas,
|
||||
pub text_renderer: TextRenderer,
|
||||
pub line_buffers: Vec<Buffer>,
|
||||
|
||||
pub cell_width: f32,
|
||||
pub cell_height: f32,
|
||||
pub _metrics: Metrics,
|
||||
|
||||
pub window: Arc<Window>,
|
||||
}
|
||||
|
||||
impl TerminalRenderer {
|
||||
pub async fn new(window: Arc<Window>, event_loop: &ActiveEventLoop) -> Self {
|
||||
let physical_size = window.inner_size();
|
||||
let scale_factor = window.scale_factor() as f32;
|
||||
|
||||
let instance = Instance::new(InstanceDescriptor::new_with_display_handle(Box::new(
|
||||
event_loop.owned_display_handle(),
|
||||
)));
|
||||
let adapter = instance
|
||||
.request_adapter(&RequestAdapterOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let (device, queue) = adapter
|
||||
.request_device(&DeviceDescriptor::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let surface = instance
|
||||
.create_surface(window.clone())
|
||||
.expect("Create surface");
|
||||
let swapchain_format = TextureFormat::Bgra8UnormSrgb;
|
||||
let surface_config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: swapchain_format,
|
||||
width: physical_size.width,
|
||||
height: physical_size.height,
|
||||
present_mode: PresentMode::Fifo,
|
||||
alpha_mode: CompositeAlphaMode::Opaque,
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
};
|
||||
surface.configure(&device, &surface_config);
|
||||
|
||||
let mut font_system = FontSystem::new();
|
||||
let swash_cache = SwashCache::new();
|
||||
let cache = Cache::new(&device);
|
||||
let viewport = Viewport::new(&device, &cache);
|
||||
let mut atlas = TextAtlas::new(&device, &queue, &cache, swapchain_format);
|
||||
let text_renderer =
|
||||
TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
|
||||
|
||||
let metrics = Metrics::new(FONT_SIZE * scale_factor, FONT_SIZE * 1.4 * scale_factor);
|
||||
let mut measure_buffer = Buffer::new(&mut font_system, metrics);
|
||||
measure_buffer.set_size(&mut font_system, Some(1000.0), Some(1000.0));
|
||||
measure_buffer.set_text(
|
||||
&mut font_system,
|
||||
"M",
|
||||
&Attrs::new().family(Family::Monospace),
|
||||
Shaping::Basic,
|
||||
None,
|
||||
);
|
||||
measure_buffer.shape_until_scroll(&mut font_system, false);
|
||||
|
||||
let cell_width = measure_buffer
|
||||
.lines
|
||||
.first()
|
||||
.and_then(|l| l.layout_opt())
|
||||
.and_then(|layouts| layouts.first())
|
||||
.map(|layout| layout.w)
|
||||
.unwrap_or(FONT_SIZE * scale_factor * 0.6);
|
||||
|
||||
let cell_height = metrics.line_height;
|
||||
|
||||
let max_lines = 200;
|
||||
let mut line_buffers = Vec::with_capacity(max_lines);
|
||||
for _ in 0..max_lines {
|
||||
line_buffers.push(Buffer::new(&mut font_system, metrics));
|
||||
}
|
||||
|
||||
Self {
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
surface_config,
|
||||
instance,
|
||||
font_system,
|
||||
swash_cache,
|
||||
viewport,
|
||||
atlas,
|
||||
text_renderer,
|
||||
line_buffers,
|
||||
cell_width,
|
||||
cell_height,
|
||||
_metrics: metrics,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: PhysicalSize<u32>) {
|
||||
self.surface_config.width = size.width;
|
||||
self.surface_config.height = size.height;
|
||||
self.surface.configure(&self.device, &self.surface_config);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
lines: &[TerminalLine],
|
||||
cursor: Option<CursorInfo>,
|
||||
) {
|
||||
let width = self.surface_config.width;
|
||||
let height = self.surface_config.height;
|
||||
|
||||
self.viewport.update(
|
||||
&self.queue,
|
||||
Resolution { width, height },
|
||||
);
|
||||
|
||||
let scale_factor = self.window.scale_factor() as f32;
|
||||
let buf_width = width as f32 * scale_factor;
|
||||
|
||||
// Phase 1: Update all line buffers.
|
||||
for (line_idx, line) in lines.iter().enumerate() {
|
||||
if line_idx >= self.line_buffers.len() {
|
||||
break;
|
||||
}
|
||||
let buffer = &mut self.line_buffers[line_idx];
|
||||
buffer.set_size(&mut self.font_system, Some(buf_width), Some(self.cell_height));
|
||||
|
||||
if line.spans.is_empty() {
|
||||
buffer.set_text(&mut self.font_system, " ", &Attrs::new(), Shaping::Basic, None);
|
||||
} else {
|
||||
let spans_iter = line.spans.iter().map(|span| {
|
||||
let color = Color::rgb(span.fg.r, span.fg.g, span.fg.b);
|
||||
let attrs = Attrs::new().family(Family::Monospace).color(color);
|
||||
(span.text.as_str(), attrs)
|
||||
});
|
||||
buffer.set_rich_text(&mut self.font_system, spans_iter, &Attrs::new(), Shaping::Basic, None);
|
||||
}
|
||||
buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
}
|
||||
|
||||
// Phase 2: Build text areas.
|
||||
let text_areas: Vec<TextArea> = self
|
||||
.line_buffers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(lines.len())
|
||||
.map(|(line_idx, buffer)| {
|
||||
let top = line_idx as f32 * self.cell_height;
|
||||
TextArea {
|
||||
buffer,
|
||||
left: 0.0,
|
||||
top,
|
||||
scale: 1.0,
|
||||
bounds: TextBounds {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: width as i32,
|
||||
bottom: height as i32,
|
||||
},
|
||||
default_color: Color::rgb(255, 255, 255),
|
||||
custom_glyphs: &[],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Phase 3: Prepare.
|
||||
if let Err(e) = self.text_renderer.prepare(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
&mut self.font_system,
|
||||
&mut self.atlas,
|
||||
&self.viewport,
|
||||
text_areas,
|
||||
&mut self.swash_cache,
|
||||
) {
|
||||
log::warn!("Text prepare error: {:?}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let frame = match self.surface.get_current_texture() {
|
||||
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
|
||||
wgpu::CurrentSurfaceTexture::Timeout
|
||||
| wgpu::CurrentSurfaceTexture::Occluded => {
|
||||
self.window.request_redraw();
|
||||
return;
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Outdated
|
||||
| wgpu::CurrentSurfaceTexture::Suboptimal(_) => {
|
||||
self.surface.configure(&self.device, &self.surface_config);
|
||||
self.window.request_redraw();
|
||||
return;
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Lost => {
|
||||
self.surface = self.instance.create_surface(self.window.clone()).unwrap();
|
||||
self.surface.configure(&self.device, &self.surface_config);
|
||||
self.window.request_redraw();
|
||||
return;
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Validation => panic!("validation error"),
|
||||
};
|
||||
|
||||
let view = frame.texture.create_view(&TextureViewDescriptor::default());
|
||||
let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor { label: None });
|
||||
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(wgpu::Color {
|
||||
r: 0.05,
|
||||
g: 0.05,
|
||||
b: 0.05,
|
||||
a: 1.0,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
if let Some(cursor) = cursor {
|
||||
if cursor.visible && cursor.y < lines.len() {
|
||||
let _x = cursor.x as f32 * self.cell_width;
|
||||
let _y = cursor.y as f32 * self.cell_height;
|
||||
// TODO: draw cursor quad.
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = self.text_renderer.render(&self.atlas, &self.viewport, &mut pass) {
|
||||
log::warn!("Text render error: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
frame.present();
|
||||
self.atlas.trim();
|
||||
}
|
||||
}
|
||||
128
src/terminal.rs
Normal file
128
src/terminal.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use alacritty_terminal::{
|
||||
event::{Event, EventListener, WindowSize},
|
||||
event_loop::{EventLoop, Msg},
|
||||
sync::FairMutex,
|
||||
term::{Config, Term},
|
||||
tty::{self, Options},
|
||||
};
|
||||
use std::{
|
||||
io,
|
||||
sync::{Arc, mpsc},
|
||||
};
|
||||
|
||||
/// Proxy that forwards alacritty_terminal events into an MPSC channel.
|
||||
#[derive(Clone)]
|
||||
pub struct EventProxy {
|
||||
sender: mpsc::Sender<Event>,
|
||||
}
|
||||
|
||||
impl EventListener for EventProxy {
|
||||
fn send_event(&self, event: Event) {
|
||||
let _ = self.sender.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// High-level wrapper around alacritty_terminal.
|
||||
pub struct TerminalEmulator {
|
||||
terminal: Arc<FairMutex<Term<EventProxy>>>,
|
||||
event_sender: alacritty_terminal::event_loop::EventLoopSender,
|
||||
event_receiver: mpsc::Receiver<Event>,
|
||||
}
|
||||
|
||||
impl TerminalEmulator {
|
||||
pub fn new(columns: usize, lines: usize) -> io::Result<Self> {
|
||||
let (event_sender, event_receiver) = mpsc::channel();
|
||||
let proxy = EventProxy {
|
||||
sender: event_sender,
|
||||
};
|
||||
|
||||
let size = TermSize {
|
||||
columns,
|
||||
screen_lines: lines,
|
||||
};
|
||||
|
||||
let term = Term::new(Config::default(), &size, proxy.clone());
|
||||
let terminal = Arc::new(FairMutex::new(term));
|
||||
|
||||
let window_size = WindowSize {
|
||||
num_lines: lines as u16,
|
||||
num_cols: columns as u16,
|
||||
cell_width: 10,
|
||||
cell_height: 20,
|
||||
};
|
||||
|
||||
let pty = tty::new(&Options::default(), window_size, 0)?;
|
||||
|
||||
let event_loop = EventLoop::new(terminal.clone(), proxy, pty, false, false)?;
|
||||
let tx = event_loop.channel();
|
||||
|
||||
// Spawn the PTY reader thread.
|
||||
let _handle = event_loop.spawn();
|
||||
|
||||
Ok(Self {
|
||||
terminal,
|
||||
event_sender: tx,
|
||||
event_receiver,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resize(&self, columns: usize, lines: usize, cell_width: u16, cell_height: u16) {
|
||||
let window_size = WindowSize {
|
||||
num_lines: lines as u16,
|
||||
num_cols: columns as u16,
|
||||
cell_width,
|
||||
cell_height,
|
||||
};
|
||||
|
||||
self.terminal.lock().resize(TermSize { columns, screen_lines: lines });
|
||||
let _ = self.event_sender.send(Msg::Resize(window_size));
|
||||
}
|
||||
|
||||
pub fn send_input(&self, data: &[u8]) {
|
||||
let _ = self
|
||||
.event_sender
|
||||
.send(Msg::Input(std::borrow::Cow::Owned(data.to_vec())));
|
||||
}
|
||||
|
||||
pub fn read_events(&self) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
while let Ok(event) = self.event_receiver.try_recv() {
|
||||
events.push(event);
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
pub fn with_term<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Term<EventProxy>) -> R,
|
||||
{
|
||||
let mut term = self.terminal.lock();
|
||||
f(&mut term)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalEmulator {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.event_sender.send(Msg::Shutdown);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct TermSize {
|
||||
pub columns: usize,
|
||||
pub screen_lines: usize,
|
||||
}
|
||||
|
||||
impl alacritty_terminal::grid::Dimensions for TermSize {
|
||||
fn total_lines(&self) -> usize {
|
||||
self.screen_lines
|
||||
}
|
||||
|
||||
fn screen_lines(&self) -> usize {
|
||||
self.screen_lines
|
||||
}
|
||||
|
||||
fn columns(&self) -> usize {
|
||||
self.columns
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user