Convert to libcosmic
This commit is contained in:
parent
75d6da50c0
commit
f6118a5cee
8 changed files with 3965 additions and 870 deletions
2718
Cargo.lock
generated
2718
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
31
Cargo.toml
31
Cargo.toml
|
|
@ -7,36 +7,19 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
alacritty_terminal = "0.19"
|
||||
bytemuck = "1.14"
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
tiny-skia = "0.11"
|
||||
tokio = { version = "1.35", features = ["sync"] }
|
||||
|
||||
[dependencies.cosmic-text]
|
||||
git = "https://github.com/pop-os/cosmic-text.git"
|
||||
branch = "refactor"
|
||||
|
||||
[dependencies.glyphon]
|
||||
git = "https://github.com/jackpot51/glyphon.git"
|
||||
branch = "refactor"
|
||||
optional = true
|
||||
|
||||
[dependencies.pollster]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
|
||||
[dependencies.softbuffer]
|
||||
git = "https://github.com/pop-os/softbuffer.git"
|
||||
branch = "cosmic"
|
||||
|
||||
[dependencies.wgpu]
|
||||
version = "0.18"
|
||||
optional = true
|
||||
|
||||
[dependencies.winit]
|
||||
git = "https://github.com/pop-os/winit.git"
|
||||
branch = "master"
|
||||
[dependencies.libcosmic]
|
||||
git = "https://github.com/pop-os/libcosmic.git"
|
||||
default-features = false
|
||||
features = ["tokio", "winit"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
wgpu = ["dep:glyphon", "dep:pollster", "dep:wgpu"]
|
||||
default = ["wgpu"]
|
||||
wgpu = ["libcosmic/wgpu"]
|
||||
|
|
|
|||
642
src/main.rs
642
src/main.rs
|
|
@ -1,430 +1,282 @@
|
|||
use alacritty_terminal::{
|
||||
ansi::{Color, NamedColor},
|
||||
config::{Config, PtyConfig},
|
||||
event::{Event as TermEvent, EventListener, Notify, OnResize, WindowSize},
|
||||
event_loop::{EventLoop as PtyEventLoop, Notifier},
|
||||
grid::Dimensions,
|
||||
index::{Column, Line, Point},
|
||||
sync::FairMutex,
|
||||
term::{
|
||||
cell::Flags,
|
||||
color::{Colors, Rgb},
|
||||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use alacritty_terminal::event::Event as TermEvent;
|
||||
use cosmic::{
|
||||
app::{Command, Core, Settings},
|
||||
cosmic_theme, executor,
|
||||
iced::{
|
||||
self,
|
||||
futures::SinkExt,
|
||||
subscription::{self, Subscription},
|
||||
widget::row,
|
||||
window, Alignment, Length,
|
||||
},
|
||||
tty, Term,
|
||||
};
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, Buffer, BufferLine, Family, FontSystem, Metrics, Shaping, Style, SwashCache,
|
||||
Weight, Wrap,
|
||||
};
|
||||
use std::{mem, rc::Rc, sync::Arc, time::Instant};
|
||||
use winit::{
|
||||
event::{
|
||||
ElementState, Event as WinitEvent, KeyboardInput, ModifiersState, VirtualKeyCode,
|
||||
WindowEvent,
|
||||
},
|
||||
event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy},
|
||||
window::WindowBuilder,
|
||||
iced_core::Size,
|
||||
style,
|
||||
widget::{self, segmented_button},
|
||||
ApplicationExt, Element,
|
||||
};
|
||||
use std::{any::TypeId, sync::Mutex};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use self::renderer::Renderer;
|
||||
mod renderer;
|
||||
use self::terminal::Terminal;
|
||||
mod terminal;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Size {
|
||||
width: f32,
|
||||
height: f32,
|
||||
cell_width: f32,
|
||||
cell_height: f32,
|
||||
}
|
||||
use self::terminal_box::terminal_box;
|
||||
mod terminal_box;
|
||||
|
||||
impl Dimensions for Size {
|
||||
fn total_lines(&self) -> usize {
|
||||
self.screen_lines()
|
||||
}
|
||||
|
||||
fn screen_lines(&self) -> usize {
|
||||
(self.height / self.cell_height).floor() as usize
|
||||
}
|
||||
|
||||
fn columns(&self) -> usize {
|
||||
(self.width / self.cell_width).floor() as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size> for WindowSize {
|
||||
fn from(size: Size) -> Self {
|
||||
Self {
|
||||
num_lines: size.screen_lines() as u16,
|
||||
num_cols: size.columns() as u16,
|
||||
cell_width: size.cell_width as u16,
|
||||
cell_height: size.cell_height as u16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EventProxy(EventLoopProxy<TermEvent>);
|
||||
|
||||
impl EventListener for EventProxy {
|
||||
fn send_event(&self, event: TermEvent) {
|
||||
let _ = self.0.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn colors() -> Colors {
|
||||
let mut colors = Colors::default();
|
||||
|
||||
// These colors come from `ransid`: https://gitlab.redox-os.org/redox-os/ransid/-/blob/master/src/color.rs
|
||||
let encode_rgb = |r: u8, g: u8, b: u8| -> Rgb { Rgb { r, g, b } };
|
||||
for value in 0..=255 {
|
||||
let color = match value {
|
||||
0 => encode_rgb(0x00, 0x00, 0x00),
|
||||
1 => encode_rgb(0x80, 0x00, 0x00),
|
||||
2 => encode_rgb(0x00, 0x80, 0x00),
|
||||
3 => encode_rgb(0x80, 0x80, 0x00),
|
||||
4 => encode_rgb(0x00, 0x00, 0x80),
|
||||
5 => encode_rgb(0x80, 0x00, 0x80),
|
||||
6 => encode_rgb(0x00, 0x80, 0x80),
|
||||
7 => encode_rgb(0xc0, 0xc0, 0xc0),
|
||||
8 => encode_rgb(0x80, 0x80, 0x80),
|
||||
9 => encode_rgb(0xff, 0x00, 0x00),
|
||||
10 => encode_rgb(0x00, 0xff, 0x00),
|
||||
11 => encode_rgb(0xff, 0xff, 0x00),
|
||||
12 => encode_rgb(0x00, 0x00, 0xff),
|
||||
13 => encode_rgb(0xff, 0x00, 0xff),
|
||||
14 => encode_rgb(0x00, 0xff, 0xff),
|
||||
15 => encode_rgb(0xff, 0xff, 0xff),
|
||||
16..=231 => {
|
||||
let convert = |value: u8| -> u8 {
|
||||
match value {
|
||||
0 => 0,
|
||||
_ => value * 0x28 + 0x28,
|
||||
}
|
||||
};
|
||||
|
||||
let r = convert((value - 16) / 36 % 6);
|
||||
let g = convert((value - 16) / 6 % 6);
|
||||
let b = convert((value - 16) % 6);
|
||||
encode_rgb(r, g, b)
|
||||
}
|
||||
232..=255 => {
|
||||
let gray = (value - 232) * 10 + 8;
|
||||
encode_rgb(gray, gray, gray)
|
||||
}
|
||||
};
|
||||
colors[value as usize] = Some(color);
|
||||
}
|
||||
|
||||
// Set special colors
|
||||
colors[NamedColor::Foreground] = colors[NamedColor::White];
|
||||
colors[NamedColor::Background] = colors[NamedColor::Black];
|
||||
/*TODO
|
||||
colors[NamedColor::Cursor] = colors[NamedColor::];
|
||||
colors[NamedColor::DimBlack] = colors[NamedColor::];
|
||||
colors[NamedColor::DimRed] = colors[NamedColor::];
|
||||
colors[NamedColor::DimGreen] = colors[NamedColor::];
|
||||
colors[NamedColor::DimYellow] = colors[NamedColor::];
|
||||
colors[NamedColor::DimBlue] = colors[NamedColor::];
|
||||
colors[NamedColor::DimMagenta] = colors[NamedColor::];
|
||||
colors[NamedColor::DimCyan] = colors[NamedColor::];
|
||||
colors[NamedColor::DimWhite] = colors[NamedColor::];
|
||||
*/
|
||||
colors[NamedColor::BrightForeground] = colors[NamedColor::BrightWhite];
|
||||
//TODO colors[NamedColor::DimForeground] = colors[NamedColor::];
|
||||
|
||||
colors
|
||||
}
|
||||
|
||||
fn main() {
|
||||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
|
||||
let mut font_system = FontSystem::new();
|
||||
let mut swash_cache = SwashCache::new();
|
||||
let metrics = Metrics::new(14.0, 20.0);
|
||||
//TODO: set color to default fg
|
||||
let default_attrs = Attrs::new().family(Family::Monospace);
|
||||
let mut buffer = Buffer::new_empty(metrics);
|
||||
buffer.set_wrap(&mut font_system, Wrap::None);
|
||||
let (cell_width, cell_height) = {
|
||||
// Use size of space to determine cell size
|
||||
buffer.set_text(&mut font_system, " ", default_attrs, Shaping::Advanced);
|
||||
let layout = buffer.line_layout(&mut font_system, 0).unwrap();
|
||||
(layout[0].w, metrics.line_height)
|
||||
};
|
||||
println!("{}, {}", cell_width, cell_height);
|
||||
|
||||
let event_loop = EventLoopBuilder::<TermEvent>::with_user_event().build();
|
||||
let event_loop_proxy = event_loop.create_proxy();
|
||||
let window = Rc::new(WindowBuilder::new().build(&event_loop).unwrap());
|
||||
let (width, height) = {
|
||||
let inner_size = window.inner_size();
|
||||
(inner_size.width as f32, inner_size.height as f32)
|
||||
let settings = Settings::default()
|
||||
.antialiasing(true)
|
||||
.client_decorations(true)
|
||||
.debug(false)
|
||||
.default_icon_theme("Pop")
|
||||
.default_text_size(16.0)
|
||||
.scale_factor(1.0)
|
||||
.size(Size::new(1024., 768.))
|
||||
.theme(cosmic::Theme::dark());
|
||||
|
||||
cosmic::app::run::<App>(settings, ())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Messages that are used specifically by our [`App`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
TabActivate(segmented_button::Entity),
|
||||
TabClose(segmented_button::Entity),
|
||||
TabNew,
|
||||
TermEvent(segmented_button::Entity, TermEvent),
|
||||
TermEventTx(mpsc::Sender<(segmented_button::Entity, TermEvent)>),
|
||||
}
|
||||
|
||||
/// The [`App`] stores application-specific state.
|
||||
pub struct App {
|
||||
core: Core,
|
||||
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
|
||||
term_event_tx_opt: Option<mpsc::Sender<(segmented_button::Entity, TermEvent)>>,
|
||||
}
|
||||
|
||||
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
||||
impl cosmic::Application for App {
|
||||
/// Default async executor to use with the app.
|
||||
type Executor = executor::Default;
|
||||
|
||||
/// Argument received [`cosmic::Application::new`].
|
||||
type Flags = ();
|
||||
|
||||
/// Message type specific to our [`App`].
|
||||
type Message = Message;
|
||||
|
||||
/// The unique application ID to supply to the window manager.
|
||||
const APP_ID: &'static str = "org.cosmic.AppDemo";
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
/// Creates the application, and optionally emits command on initialize.
|
||||
fn init(core: Core, input: Self::Flags) -> (Self, Command<Self::Message>) {
|
||||
let mut app = App {
|
||||
core,
|
||||
tab_model: segmented_button::ModelBuilder::default().build(),
|
||||
term_event_tx_opt: None,
|
||||
};
|
||||
|
||||
let config = Config::default();
|
||||
let mut dimensions = Size {
|
||||
width,
|
||||
height,
|
||||
cell_width,
|
||||
cell_height,
|
||||
};
|
||||
let event_proxy = EventProxy(event_loop_proxy);
|
||||
let term = Arc::new(FairMutex::new(Term::new(
|
||||
&config,
|
||||
&dimensions,
|
||||
event_proxy.clone(),
|
||||
)));
|
||||
let colors = colors();
|
||||
let command = app.update_title();
|
||||
|
||||
let pty_config = PtyConfig::default();
|
||||
let window_id = 0;
|
||||
let pty = tty::new(&pty_config, dimensions.into(), window_id).unwrap();
|
||||
|
||||
let pty_event_loop = PtyEventLoop::new(term.clone(), event_proxy, pty, pty_config.hold, false);
|
||||
let mut notifier = Notifier(pty_event_loop.channel());
|
||||
let pty_join_handle = pty_event_loop.spawn();
|
||||
|
||||
let mut renderer = Renderer::new(window.clone()).unwrap();
|
||||
let mut modifiers = ModifiersState::default();
|
||||
event_loop.run(move |event, _elwt, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match event {
|
||||
WinitEvent::RedrawRequested(window_id) if window_id == window.id() => {
|
||||
let instant = Instant::now();
|
||||
|
||||
renderer.render(&mut buffer, &mut font_system, &mut swash_cache);
|
||||
|
||||
println!("draw {:?}", instant.elapsed());
|
||||
(app, command)
|
||||
}
|
||||
WinitEvent::WindowEvent {
|
||||
event: WindowEvent::ReceivedCharacter(c),
|
||||
window_id,
|
||||
} if window_id == window.id() => {
|
||||
match (modifiers.logo(), modifiers.ctrl(), modifiers.alt()) {
|
||||
(true, _, _) => {} // Ignore super
|
||||
(false, _, true) => {
|
||||
// Alt keys
|
||||
let mut buf = [0x1B, 0, 0, 0, 0];
|
||||
let str = c.encode_utf8(&mut buf[1..]);
|
||||
notifier.notify(str.as_bytes().to_vec());
|
||||
|
||||
/// Handle application events here.
|
||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||
match message {
|
||||
Message::TabActivate(entity) => {
|
||||
self.tab_model.activate(entity);
|
||||
return self.update_title();
|
||||
}
|
||||
(false, _, false) => {
|
||||
let mut buf = [0, 0, 0, 0];
|
||||
let str = c.encode_utf8(&mut buf);
|
||||
notifier.notify(str.as_bytes().to_vec());
|
||||
Message::TabClose(entity) => {
|
||||
// Activate closest item
|
||||
if let Some(position) = self.tab_model.position(entity) {
|
||||
if position > 0 {
|
||||
self.tab_model.activate_position(position - 1);
|
||||
} else {
|
||||
self.tab_model.activate_position(position + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove item
|
||||
self.tab_model.remove(entity);
|
||||
|
||||
// If that was the last tab, close window
|
||||
if self.tab_model.iter().next().is_none() {
|
||||
return window::close(window::Id::MAIN);
|
||||
}
|
||||
|
||||
return self.update_title();
|
||||
}
|
||||
Message::TabNew => match &self.term_event_tx_opt {
|
||||
Some(term_event_tx) => {
|
||||
let entity = self
|
||||
.tab_model
|
||||
.insert()
|
||||
.text("New Terminal")
|
||||
.closable()
|
||||
.activate()
|
||||
.id();
|
||||
let terminal = Terminal::new(entity, term_event_tx.clone());
|
||||
self.tab_model
|
||||
.data_set::<Mutex<Terminal>>(entity, Mutex::new(terminal));
|
||||
}
|
||||
None => {
|
||||
log::warn!("tried to create new tab before having event channel");
|
||||
}
|
||||
WinitEvent::WindowEvent {
|
||||
event:
|
||||
WindowEvent::KeyboardInput {
|
||||
input:
|
||||
KeyboardInput {
|
||||
state: ElementState::Pressed,
|
||||
virtual_keycode: Some(virtual_keycode),
|
||||
..
|
||||
},
|
||||
..
|
||||
},
|
||||
window_id,
|
||||
} if window_id == window.id() => {
|
||||
match (modifiers.logo(), modifiers.ctrl(), modifiers.alt()) {
|
||||
(true, _, _) => {} // Ignore super
|
||||
(false, true, _) => {
|
||||
// Control keys will use ReceivedCharacter instead
|
||||
}
|
||||
(false, false, true) => {
|
||||
//TODO: support Alt keys without character
|
||||
}
|
||||
(false, false, false) => match virtual_keycode {
|
||||
VirtualKeyCode::Up => {
|
||||
notifier.notify(b"\x1B[A".as_slice());
|
||||
}
|
||||
VirtualKeyCode::Down => {
|
||||
notifier.notify(b"\x1B[B".as_slice());
|
||||
}
|
||||
VirtualKeyCode::Right => {
|
||||
notifier.notify(b"\x1B[C".as_slice());
|
||||
}
|
||||
VirtualKeyCode::Left => {
|
||||
notifier.notify(b"\x1B[D".as_slice());
|
||||
}
|
||||
VirtualKeyCode::End => {
|
||||
notifier.notify(b"\x1B[F".as_slice());
|
||||
}
|
||||
VirtualKeyCode::Home => {
|
||||
notifier.notify(b"\x1B[H".as_slice());
|
||||
}
|
||||
_ => {}
|
||||
Message::TermEvent(entity, event) => match event {
|
||||
TermEvent::Bell => {
|
||||
//TODO: audible or visible bell options?
|
||||
}
|
||||
TermEvent::ColorRequest(index, f) => {
|
||||
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
let terminal = terminal.lock().unwrap();
|
||||
let rgb = terminal.colors()[index].unwrap_or_default();
|
||||
let text = f(rgb);
|
||||
terminal.input(text.into_bytes());
|
||||
}
|
||||
}
|
||||
TermEvent::Exit => {
|
||||
return self.update(Message::TabClose(entity));
|
||||
}
|
||||
TermEvent::PtyWrite(text) => {
|
||||
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
let terminal = terminal.lock().unwrap();
|
||||
terminal.input(text.into_bytes());
|
||||
}
|
||||
}
|
||||
TermEvent::ResetTitle => {
|
||||
self.tab_model.text_set(entity, "New Terminal");
|
||||
return self.update_title();
|
||||
}
|
||||
TermEvent::TextAreaSizeRequest(f) => {
|
||||
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
let terminal = terminal.lock().unwrap();
|
||||
let text = f(terminal.size().into());
|
||||
terminal.input(text.into_bytes());
|
||||
}
|
||||
}
|
||||
TermEvent::Title(title) => {
|
||||
self.tab_model.text_set(entity, title);
|
||||
return self.update_title();
|
||||
}
|
||||
TermEvent::Wakeup => {
|
||||
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
let mut terminal = terminal.lock().unwrap();
|
||||
terminal.update();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("TODO: {:?}", event);
|
||||
}
|
||||
},
|
||||
Message::TermEventTx(term_event_tx) => {
|
||||
self.term_event_tx_opt = Some(term_event_tx);
|
||||
}
|
||||
}
|
||||
WinitEvent::WindowEvent {
|
||||
event: WindowEvent::ModifiersChanged(new_modifiers),
|
||||
window_id,
|
||||
} if window_id == window.id() => {
|
||||
modifiers = new_modifiers;
|
||||
}
|
||||
WinitEvent::WindowEvent {
|
||||
event: WindowEvent::Resized(physical_size),
|
||||
window_id,
|
||||
} if window_id == window.id() => {
|
||||
let instant = Instant::now();
|
||||
|
||||
dimensions.width = physical_size.width as f32;
|
||||
dimensions.height = physical_size.height as f32;
|
||||
Command::none()
|
||||
}
|
||||
|
||||
notifier.on_resize(dimensions.into());
|
||||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<Self::Message> {
|
||||
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
|
||||
|
||||
term.lock().resize(dimensions);
|
||||
let mut tab_column = widget::column::with_capacity(1);
|
||||
|
||||
buffer.set_size(
|
||||
&mut font_system,
|
||||
dimensions.width as f32,
|
||||
dimensions.height as f32,
|
||||
tab_column = tab_column.push(
|
||||
row![
|
||||
widget::view_switcher::horizontal(&self.tab_model)
|
||||
.button_height(32)
|
||||
.button_spacing(space_xxs)
|
||||
.on_activate(Message::TabActivate)
|
||||
.on_close(Message::TabClose)
|
||||
.width(Length::Shrink),
|
||||
widget::button(widget::icon::from_name("list-add-symbolic").size(16).icon())
|
||||
.on_press(Message::TabNew)
|
||||
.padding(space_xxs)
|
||||
.style(style::Button::Icon)
|
||||
]
|
||||
.align_items(Alignment::Center),
|
||||
);
|
||||
|
||||
renderer.resize(physical_size.width, physical_size.height);
|
||||
|
||||
println!("resize {:?}", instant.elapsed());
|
||||
}
|
||||
WinitEvent::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
window_id,
|
||||
} if window_id == window.id() => {
|
||||
term.lock().exit();
|
||||
}
|
||||
WinitEvent::UserEvent(user_event) => {
|
||||
println!("{:?}", user_event);
|
||||
match user_event {
|
||||
//TODO: other error codes?
|
||||
TermEvent::Exit => *control_flow = ControlFlow::ExitWithCode(0),
|
||||
TermEvent::PtyWrite(text) => notifier.notify(text.into_bytes()),
|
||||
TermEvent::Title(title) => {
|
||||
window.set_title(&title);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let instant = Instant::now();
|
||||
|
||||
//TODO: is redraw needed after all events?
|
||||
//TODO: use LineDamageBounds
|
||||
match self
|
||||
.tab_model
|
||||
.data::<Mutex<Terminal>>(self.tab_model.active())
|
||||
{
|
||||
let mut last_point = Point::new(Line(0), Column(0));
|
||||
let mut text = String::new();
|
||||
let mut attrs_list = AttrsList::new(default_attrs);
|
||||
let term_guard = term.lock();
|
||||
let grid = term_guard.grid();
|
||||
for indexed in grid.display_iter() {
|
||||
if indexed.point.line != last_point.line {
|
||||
let line_i = last_point.line.0 as usize;
|
||||
while line_i >= buffer.lines.len() {
|
||||
buffer.lines.push(BufferLine::new(
|
||||
"",
|
||||
AttrsList::new(default_attrs),
|
||||
Shaping::Advanced,
|
||||
));
|
||||
buffer.set_redraw(true);
|
||||
Some(terminal) => {
|
||||
//TODO
|
||||
tab_column = tab_column.push(terminal_box(terminal));
|
||||
}
|
||||
|
||||
if buffer.lines[line_i].set_text(text.clone(), attrs_list.clone()) {
|
||||
buffer.set_redraw(true);
|
||||
}
|
||||
|
||||
text.clear();
|
||||
attrs_list.clear_spans();
|
||||
}
|
||||
//TODO: use indexed.point.column?
|
||||
|
||||
//TODO: skip leading spacer?
|
||||
if indexed.cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
|
||||
// Skip wide spacers (cells after wide characters)
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = text.len();
|
||||
text.push(indexed.cell.c);
|
||||
if let Some(zerowidth) = indexed.cell.zerowidth() {
|
||||
for &c in zerowidth {
|
||||
text.push(c);
|
||||
}
|
||||
}
|
||||
let end = text.len();
|
||||
|
||||
let convert_color = |color| {
|
||||
let rgb = match color {
|
||||
Color::Named(named_color) => match colors[named_color] {
|
||||
Some(rgb) => rgb,
|
||||
None => {
|
||||
eprintln!("missing named color {:?}", named_color);
|
||||
Rgb::default()
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
|
||||
let content: Element<_> = tab_column.into();
|
||||
|
||||
// Uncomment to debug layout:
|
||||
//content.explain(cosmic::iced::Color::WHITE)
|
||||
content
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
struct TerminalEventWorker;
|
||||
subscription::channel(
|
||||
TypeId::of::<TerminalEventWorker>(),
|
||||
100,
|
||||
|mut output| async move {
|
||||
let (event_tx, mut event_rx) = mpsc::channel(100);
|
||||
output.send(Message::TermEventTx(event_tx)).await.unwrap();
|
||||
|
||||
// Create first terminal tab
|
||||
output.send(Message::TabNew).await.unwrap();
|
||||
|
||||
while let Some((entity, event)) = event_rx.recv().await {
|
||||
output
|
||||
.send(Message::TermEvent(entity, event))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
panic!("terminal event channel closed");
|
||||
},
|
||||
Color::Spec(rgb) => rgb,
|
||||
Color::Indexed(index) => match colors[index as usize] {
|
||||
Some(rgb) => rgb,
|
||||
None => {
|
||||
eprintln!("missing indexed color {}", index);
|
||||
Rgb::default()
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
impl App
|
||||
where
|
||||
Self: cosmic::Application,
|
||||
{
|
||||
fn update_title(&mut self) -> Command<Message> {
|
||||
let (header_title, window_title) = match self.tab_model.text(self.tab_model.active()) {
|
||||
Some(tab_title) => (
|
||||
tab_title.to_string(),
|
||||
format!("{tab_title} — COSMIC Terminal"),
|
||||
),
|
||||
None => ("No Tab Open".to_string(), "COSMIC Terminal".to_string()),
|
||||
};
|
||||
cosmic_text::Color::rgb(rgb.r, rgb.g, rgb.b)
|
||||
};
|
||||
|
||||
let mut attrs = default_attrs;
|
||||
let mut fg = convert_color(indexed.cell.fg);
|
||||
let mut bg = convert_color(indexed.cell.bg);
|
||||
//TODO: better handling of cursor
|
||||
if indexed.point == grid.cursor.point {
|
||||
mem::swap(&mut fg, &mut bg);
|
||||
}
|
||||
attrs = attrs.color(fg);
|
||||
// Use metadata as background color
|
||||
attrs = attrs.metadata(bg.0 as usize);
|
||||
//TODO: more flags
|
||||
if indexed.cell.flags.contains(Flags::BOLD) {
|
||||
attrs = attrs.weight(Weight::BOLD);
|
||||
}
|
||||
if indexed.cell.flags.contains(Flags::ITALIC) {
|
||||
attrs = attrs.style(Style::Italic);
|
||||
}
|
||||
if attrs != attrs_list.defaults() {
|
||||
attrs_list.add_span(start..end, attrs);
|
||||
}
|
||||
|
||||
last_point = indexed.point;
|
||||
}
|
||||
|
||||
//TODO: do not repeat!
|
||||
let line_i = last_point.line.0 as usize;
|
||||
while line_i >= buffer.lines.len() {
|
||||
buffer.lines.push(BufferLine::new(
|
||||
"",
|
||||
AttrsList::new(default_attrs),
|
||||
Shaping::Advanced,
|
||||
));
|
||||
buffer.set_redraw(true);
|
||||
}
|
||||
|
||||
if buffer.lines[line_i].set_text(text, attrs_list) {
|
||||
buffer.set_redraw(true);
|
||||
self.set_header_title(header_title);
|
||||
self.set_window_title(window_title)
|
||||
}
|
||||
}
|
||||
|
||||
buffer.shape_until_scroll(&mut font_system, true);
|
||||
|
||||
if buffer.redraw() {
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
println!("buffer update {:?}", instant.elapsed());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
//TODO: hangs after event loop exit pty_join_handle.join().unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
use cosmic_text::{Buffer, FontSystem, SwashCache};
|
||||
use std::rc::Rc;
|
||||
use winit::window::Window;
|
||||
|
||||
pub use self::software::SoftwareRenderer;
|
||||
pub mod software;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub use self::wgpu::WgpuRenderer;
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub mod wgpu;
|
||||
|
||||
pub enum Renderer {
|
||||
Software(SoftwareRenderer),
|
||||
#[cfg(feature = "wgpu")]
|
||||
Wgpu(WgpuRenderer),
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new(window: Rc<Window>) -> Result<Self, String> {
|
||||
#[cfg(feature = "wgpu")]
|
||||
match WgpuRenderer::new(window.clone()) {
|
||||
Ok(renderer) => return Ok(Self::Wgpu(renderer)),
|
||||
Err(err) => {
|
||||
log::error!("failed to use hardware rendering: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
SoftwareRenderer::new(window).map(Renderer::Software)
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
font_system: &mut FontSystem,
|
||||
swash_cache: &mut SwashCache,
|
||||
) {
|
||||
match self {
|
||||
Self::Software(renderer) => {
|
||||
renderer.render(buffer, font_system, swash_cache);
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
Self::Wgpu(renderer) => {
|
||||
renderer.render(buffer, font_system, swash_cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
match self {
|
||||
Self::Software(renderer) => {
|
||||
renderer.resize(width, height);
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
Self::Wgpu(renderer) => {
|
||||
renderer.resize(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
use cosmic_text::{Buffer, FontSystem, SwashCache, SwashContent};
|
||||
use std::{rc::Rc, slice};
|
||||
use tiny_skia::{ColorU8, Paint, Pixmap, PixmapPaint, PixmapRef, Rect, Transform};
|
||||
use winit::window::Window;
|
||||
|
||||
pub struct SoftwareRenderer {
|
||||
pub window: Rc<Window>,
|
||||
pub context: softbuffer::GraphicsContext,
|
||||
}
|
||||
|
||||
impl SoftwareRenderer {
|
||||
pub fn new(window: Rc<Window>) -> Result<Self, String> {
|
||||
let context = unsafe { softbuffer::GraphicsContext::new(&*window, &*window) }
|
||||
.map_err(|err| format!("failed to create context: {}", err))?;
|
||||
Ok(Self { window, context })
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
font_system: &mut FontSystem,
|
||||
swash_cache: &mut SwashCache,
|
||||
) {
|
||||
let (width, height) = {
|
||||
let size = self.window.inner_size();
|
||||
(size.width, size.height)
|
||||
};
|
||||
|
||||
let mut pixmap = Pixmap::new(width, height).unwrap();
|
||||
//TODO: configurable background
|
||||
pixmap.fill(tiny_skia::Color::from_rgba8(0, 0, 0, 0xFF));
|
||||
|
||||
let line_height = buffer.metrics().line_height;
|
||||
let mut paint = Paint::default();
|
||||
paint.anti_alias = false;
|
||||
let pixmap_paint = PixmapPaint::default();
|
||||
let transform = Transform::identity();
|
||||
for run in buffer.layout_runs() {
|
||||
for glyph in run.glyphs.iter() {
|
||||
let physical_glyph = glyph.physical((0., 0.), 1.0);
|
||||
|
||||
let glyph_color = match glyph.color_opt {
|
||||
Some(some) => some,
|
||||
None => cosmic_text::Color::rgb(0xFF, 0xFF, 0xFF),
|
||||
};
|
||||
|
||||
let background_color = cosmic_text::Color(glyph.metadata as u32);
|
||||
if background_color.0 != 0xFF000000 {
|
||||
//TODO: Have to swap RGB for BGR
|
||||
paint.set_color_rgba8(
|
||||
background_color.b(),
|
||||
background_color.g(),
|
||||
background_color.r(),
|
||||
background_color.a(),
|
||||
);
|
||||
pixmap.fill_rect(
|
||||
Rect::from_xywh(glyph.x, run.line_top, glyph.w, line_height).unwrap(),
|
||||
&paint,
|
||||
transform,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
match swash_cache.get_image(font_system, physical_glyph.cache_key) {
|
||||
Some(image) if !image.data.is_empty() => {
|
||||
let mut data = Vec::with_capacity(
|
||||
(image.placement.width * image.placement.height) as usize,
|
||||
);
|
||||
match image.content {
|
||||
SwashContent::Mask => {
|
||||
let mut i = 0;
|
||||
while i < image.data.len() {
|
||||
//TODO: Have to swap RGB for BGR
|
||||
data.push(
|
||||
ColorU8::from_rgba(
|
||||
glyph_color.b(),
|
||||
glyph_color.g(),
|
||||
glyph_color.r(),
|
||||
image.data[i],
|
||||
)
|
||||
.premultiply(),
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
SwashContent::Color => {
|
||||
let mut i = 0;
|
||||
while i < image.data.len() {
|
||||
//TODO: Have to swap RGB for BGR
|
||||
data.push(
|
||||
ColorU8::from_rgba(
|
||||
image.data[i + 2],
|
||||
image.data[i + 1],
|
||||
image.data[i],
|
||||
image.data[i + 3],
|
||||
)
|
||||
.premultiply(),
|
||||
);
|
||||
i += 4;
|
||||
}
|
||||
}
|
||||
SwashContent::SubpixelMask => {
|
||||
todo!("TODO: SubpixelMask");
|
||||
}
|
||||
}
|
||||
|
||||
let glyph_pixmap = PixmapRef::from_bytes(
|
||||
unsafe {
|
||||
slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 4)
|
||||
},
|
||||
image.placement.width,
|
||||
image.placement.height,
|
||||
)
|
||||
.unwrap();
|
||||
pixmap.draw_pixmap(
|
||||
physical_glyph.x + image.placement.left,
|
||||
run.line_y as i32 + physical_glyph.y - image.placement.top,
|
||||
glyph_pixmap,
|
||||
&pixmap_paint,
|
||||
transform,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.set_redraw(false);
|
||||
|
||||
self.context.set_buffer(
|
||||
bytemuck::cast_slice(pixmap.data()),
|
||||
width as u16,
|
||||
height as u16,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
//TODO: resize image here for better performance
|
||||
self.window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
use glyphon::{
|
||||
Attrs, Buffer, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea,
|
||||
TextAtlas, TextBounds, TextRenderer,
|
||||
};
|
||||
use std::{error::Error, rc::Rc};
|
||||
use wgpu::{
|
||||
CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Features, Instance,
|
||||
InstanceDescriptor, Limits, LoadOp, MultisampleState, Operations, PresentMode, Queue,
|
||||
RenderPassColorAttachment, RenderPassDescriptor, RequestAdapterOptions, Surface,
|
||||
SurfaceConfiguration, TextureFormat, TextureUsages, TextureViewDescriptor,
|
||||
};
|
||||
use winit::window::Window;
|
||||
|
||||
pub struct WgpuRenderer {
|
||||
pub window: Rc<Window>,
|
||||
pub device: Device,
|
||||
pub queue: Queue,
|
||||
pub surface: Surface,
|
||||
pub config: SurfaceConfiguration,
|
||||
pub atlas: TextAtlas,
|
||||
pub text_renderer: TextRenderer,
|
||||
}
|
||||
|
||||
impl WgpuRenderer {
|
||||
async fn new_async(window: Rc<Window>) -> Result<Self, String> {
|
||||
let size = window.inner_size();
|
||||
|
||||
let instance = Instance::new(InstanceDescriptor::default());
|
||||
let adapter = instance
|
||||
.request_adapter(&RequestAdapterOptions::default())
|
||||
.await
|
||||
.ok_or(format!("failed to request adapter"))?;
|
||||
let (device, queue) = adapter
|
||||
.request_device(
|
||||
&DeviceDescriptor {
|
||||
label: None,
|
||||
features: Features::empty(),
|
||||
limits: Limits::downlevel_defaults(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("failed to request device: {}", err))?;
|
||||
let surface = unsafe { instance.create_surface(&*window) }
|
||||
.map_err(|err| format!("failed to create surface: {}", err))?;
|
||||
let swapchain_format = TextureFormat::Bgra8UnormSrgb;
|
||||
let mut config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: swapchain_format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: PresentMode::Fifo,
|
||||
alpha_mode: CompositeAlphaMode::Opaque,
|
||||
view_formats: vec![],
|
||||
};
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let mut atlas = TextAtlas::new(&device, &queue, swapchain_format);
|
||||
let mut text_renderer =
|
||||
TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
|
||||
Ok(Self {
|
||||
window,
|
||||
device,
|
||||
queue,
|
||||
surface,
|
||||
config,
|
||||
atlas,
|
||||
text_renderer,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(window: Rc<Window>) -> Result<Self, String> {
|
||||
pollster::block_on(Self::new_async(window))
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
font_system: &mut FontSystem,
|
||||
swash_cache: &mut SwashCache,
|
||||
) {
|
||||
self.text_renderer
|
||||
.prepare(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
font_system,
|
||||
&mut self.atlas,
|
||||
Resolution {
|
||||
width: self.config.width,
|
||||
height: self.config.height,
|
||||
},
|
||||
[TextArea {
|
||||
buffer: &buffer,
|
||||
left: 0.0,
|
||||
top: 0.0,
|
||||
scale: 1.0,
|
||||
bounds: TextBounds {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: self.config.width as i32,
|
||||
bottom: self.config.height as i32,
|
||||
},
|
||||
default_color: Color::rgb(255, 255, 255),
|
||||
}],
|
||||
swash_cache,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let frame = self.surface.get_current_texture().unwrap();
|
||||
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,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
self.text_renderer.render(&self.atlas, &mut pass).unwrap();
|
||||
}
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
frame.present();
|
||||
|
||||
self.atlas.trim();
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
self.config.width = width;
|
||||
self.config.height = height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.window.request_redraw();
|
||||
}
|
||||
}
|
||||
433
src/terminal.rs
Normal file
433
src/terminal.rs
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
use alacritty_terminal::{
|
||||
ansi::{Color, NamedColor},
|
||||
config::{Config, PtyConfig},
|
||||
event::{Event, EventListener, Notify, OnResize, WindowSize},
|
||||
event_loop::{EventLoop, Msg, Notifier, State},
|
||||
grid::Dimensions,
|
||||
index::{Column, Line, Point},
|
||||
sync::FairMutex,
|
||||
term::{
|
||||
cell::Flags,
|
||||
color::{Colors, Rgb},
|
||||
},
|
||||
tty, Term,
|
||||
};
|
||||
use cosmic::{iced::advanced::graphics::text::font_system, widget::segmented_button};
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, Buffer, BufferLine, Family, FontSystem, Metrics, Shaping, Style, Weight, Wrap,
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
mem,
|
||||
sync::{Arc, Weak},
|
||||
thread::JoinHandle,
|
||||
time::Instant,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Size {
|
||||
width: u32,
|
||||
height: u32,
|
||||
cell_width: f32,
|
||||
cell_height: f32,
|
||||
}
|
||||
|
||||
impl Dimensions for Size {
|
||||
fn total_lines(&self) -> usize {
|
||||
self.screen_lines()
|
||||
}
|
||||
|
||||
fn screen_lines(&self) -> usize {
|
||||
((self.height as f32) / self.cell_height).floor() as usize
|
||||
}
|
||||
|
||||
fn columns(&self) -> usize {
|
||||
((self.width as f32) / self.cell_width).floor() as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size> for WindowSize {
|
||||
fn from(size: Size) -> Self {
|
||||
Self {
|
||||
num_lines: size.screen_lines() as u16,
|
||||
num_cols: size.columns() as u16,
|
||||
cell_width: size.cell_width as u16,
|
||||
cell_height: size.cell_height as u16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EventProxy(
|
||||
segmented_button::Entity,
|
||||
mpsc::Sender<(segmented_button::Entity, Event)>,
|
||||
);
|
||||
|
||||
impl EventListener for EventProxy {
|
||||
fn send_event(&self, event: Event) {
|
||||
//TODO: handle error
|
||||
let _ = self.1.blocking_send((self.0, event));
|
||||
}
|
||||
}
|
||||
|
||||
fn colors() -> Colors {
|
||||
let mut colors = Colors::default();
|
||||
|
||||
// These colors come from `ransid`: https://gitlab.redox-os.org/redox-os/ransid/-/blob/master/src/color.rs
|
||||
let encode_rgb = |r: u8, g: u8, b: u8| -> Rgb { Rgb { r, g, b } };
|
||||
for value in 0..=255 {
|
||||
let color = match value {
|
||||
/* Naive colors
|
||||
0 => encode_rgb(0x00, 0x00, 0x00),
|
||||
1 => encode_rgb(0x80, 0x00, 0x00),
|
||||
2 => encode_rgb(0x00, 0x80, 0x00),
|
||||
3 => encode_rgb(0x80, 0x80, 0x00),
|
||||
4 => encode_rgb(0x00, 0x00, 0x80),
|
||||
5 => encode_rgb(0x80, 0x00, 0x80),
|
||||
6 => encode_rgb(0x00, 0x80, 0x80),
|
||||
7 => encode_rgb(0xc0, 0xc0, 0xc0),
|
||||
8 => encode_rgb(0x80, 0x80, 0x80),
|
||||
9 => encode_rgb(0xff, 0x00, 0x00),
|
||||
10 => encode_rgb(0x00, 0xff, 0x00),
|
||||
11 => encode_rgb(0xff, 0xff, 0x00),
|
||||
12 => encode_rgb(0x00, 0x00, 0xff),
|
||||
13 => encode_rgb(0xff, 0x00, 0xff),
|
||||
14 => encode_rgb(0x00, 0xff, 0xff),
|
||||
15 => encode_rgb(0xff, 0xff, 0xff),
|
||||
*/
|
||||
// Pop colors (from pop-desktop gsettings)
|
||||
0 => encode_rgb(51, 51, 51),
|
||||
1 => encode_rgb(204, 0, 0),
|
||||
2 => encode_rgb(78, 154, 6),
|
||||
3 => encode_rgb(196, 160, 0),
|
||||
4 => encode_rgb(52, 101, 164),
|
||||
5 => encode_rgb(117, 80, 123),
|
||||
6 => encode_rgb(6, 152, 154),
|
||||
7 => encode_rgb(211, 215, 207),
|
||||
8 => encode_rgb(136, 128, 124),
|
||||
9 => encode_rgb(241, 93, 34),
|
||||
10 => encode_rgb(115, 196, 143),
|
||||
11 => encode_rgb(255, 206, 81),
|
||||
12 => encode_rgb(72, 185, 199),
|
||||
13 => encode_rgb(173, 127, 168),
|
||||
14 => encode_rgb(52, 226, 226),
|
||||
15 => encode_rgb(238, 238, 236),
|
||||
/* Indexed colors */
|
||||
16..=231 => {
|
||||
let convert = |value: u8| -> u8 {
|
||||
match value {
|
||||
0 => 0,
|
||||
_ => value * 0x28 + 0x28,
|
||||
}
|
||||
};
|
||||
|
||||
let r = convert((value - 16) / 36 % 6);
|
||||
let g = convert((value - 16) / 6 % 6);
|
||||
let b = convert((value - 16) % 6);
|
||||
encode_rgb(r, g, b)
|
||||
}
|
||||
232..=255 => {
|
||||
let gray = (value - 232) * 10 + 8;
|
||||
encode_rgb(gray, gray, gray)
|
||||
}
|
||||
};
|
||||
colors[value as usize] = Some(color);
|
||||
}
|
||||
|
||||
// Set special colors
|
||||
// Pop colors (from pop-desktop gsettings)
|
||||
colors[NamedColor::Foreground] = Some(encode_rgb(242, 242, 242));
|
||||
colors[NamedColor::Background] = Some(encode_rgb(51, 51, 51));
|
||||
/*TODO
|
||||
colors[NamedColor::Cursor] = colors[NamedColor::];
|
||||
colors[NamedColor::DimBlack] = colors[NamedColor::];
|
||||
colors[NamedColor::DimRed] = colors[NamedColor::];
|
||||
colors[NamedColor::DimGreen] = colors[NamedColor::];
|
||||
colors[NamedColor::DimYellow] = colors[NamedColor::];
|
||||
colors[NamedColor::DimBlue] = colors[NamedColor::];
|
||||
colors[NamedColor::DimMagenta] = colors[NamedColor::];
|
||||
colors[NamedColor::DimCyan] = colors[NamedColor::];
|
||||
colors[NamedColor::DimWhite] = colors[NamedColor::];
|
||||
*/
|
||||
colors[NamedColor::BrightForeground] = colors[NamedColor::BrightWhite];
|
||||
//TODO colors[NamedColor::DimForeground] = colors[NamedColor::];
|
||||
|
||||
colors
|
||||
}
|
||||
|
||||
fn convert_color(colors: &Colors, color: Color) -> cosmic_text::Color {
|
||||
let rgb = match color {
|
||||
Color::Named(named_color) => match colors[named_color] {
|
||||
Some(rgb) => rgb,
|
||||
None => {
|
||||
log::warn!("missing named color {:?}", named_color);
|
||||
Rgb::default()
|
||||
}
|
||||
},
|
||||
Color::Spec(rgb) => rgb,
|
||||
Color::Indexed(index) => match colors[index as usize] {
|
||||
Some(rgb) => rgb,
|
||||
None => {
|
||||
log::warn!("missing indexed color {}", index);
|
||||
Rgb::default()
|
||||
}
|
||||
},
|
||||
};
|
||||
cosmic_text::Color::rgb(rgb.r, rgb.g, rgb.b)
|
||||
}
|
||||
|
||||
pub struct Terminal {
|
||||
metrics: Metrics,
|
||||
default_attrs: Attrs<'static>,
|
||||
buffer: Arc<Buffer>,
|
||||
size: Size,
|
||||
term: Arc<FairMutex<Term<EventProxy>>>,
|
||||
colors: Colors,
|
||||
notifier: Notifier,
|
||||
pty_join_handle: JoinHandle<(EventLoop<tty::Pty, EventProxy>, State)>,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
//TODO: error handling
|
||||
pub fn new(
|
||||
entity: segmented_button::Entity,
|
||||
event_tx: mpsc::Sender<(segmented_button::Entity, Event)>,
|
||||
) -> Self {
|
||||
let colors = colors();
|
||||
|
||||
let metrics = Metrics::new(14.0, 20.0);
|
||||
//TODO: set color to default fg
|
||||
let default_attrs = Attrs::new()
|
||||
.family(Family::Monospace)
|
||||
.color(convert_color(&colors, Color::Named(NamedColor::Foreground)))
|
||||
.metadata(convert_color(&colors, Color::Named(NamedColor::Background)).0 as usize);
|
||||
let mut buffer = Buffer::new_empty(metrics);
|
||||
|
||||
let (cell_width, cell_height) = {
|
||||
let mut font_system = font_system().write().unwrap();
|
||||
let mut font_system = font_system.raw();
|
||||
buffer.set_wrap(&mut font_system, Wrap::None);
|
||||
|
||||
// Use size of space to determine cell size
|
||||
buffer.set_text(&mut font_system, " ", default_attrs, Shaping::Advanced);
|
||||
let layout = buffer.line_layout(&mut font_system, 0).unwrap();
|
||||
(layout[0].w, metrics.line_height)
|
||||
};
|
||||
|
||||
let config = Config::default();
|
||||
let mut size = Size {
|
||||
width: (80.0 * cell_width).ceil() as u32,
|
||||
height: (24.0 * cell_height).ceil() as u32,
|
||||
cell_width,
|
||||
cell_height,
|
||||
};
|
||||
let event_proxy = EventProxy(entity, event_tx);
|
||||
let term = Arc::new(FairMutex::new(Term::new(
|
||||
&config,
|
||||
&size,
|
||||
event_proxy.clone(),
|
||||
)));
|
||||
|
||||
let window_id = 0;
|
||||
let pty = tty::new(&config.pty_config, size.into(), window_id).unwrap();
|
||||
|
||||
let pty_event_loop = EventLoop::new(
|
||||
term.clone(),
|
||||
event_proxy,
|
||||
pty,
|
||||
config.pty_config.hold,
|
||||
false,
|
||||
);
|
||||
let notifier = Notifier(pty_event_loop.channel());
|
||||
let pty_join_handle = pty_event_loop.spawn();
|
||||
|
||||
Self {
|
||||
colors,
|
||||
metrics,
|
||||
default_attrs,
|
||||
buffer: Arc::new(buffer),
|
||||
size,
|
||||
term,
|
||||
notifier,
|
||||
pty_join_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_weak(&self) -> Weak<Buffer> {
|
||||
Arc::downgrade(&self.buffer)
|
||||
}
|
||||
|
||||
/// Get the internal [`Buffer`]
|
||||
pub fn with_buffer<F: FnOnce(&Buffer) -> T, T>(&self, f: F) -> T {
|
||||
f(&self.buffer)
|
||||
}
|
||||
|
||||
/// Get the internal [`Buffer`], mutably
|
||||
pub fn with_buffer_mut<F: FnOnce(&mut Buffer) -> T, T>(&mut self, f: F) -> T {
|
||||
f(Arc::make_mut(&mut self.buffer))
|
||||
}
|
||||
|
||||
pub fn colors(&self) -> &Colors {
|
||||
&self.colors
|
||||
}
|
||||
|
||||
pub fn default_attrs(&self) -> &Attrs<'static> {
|
||||
&self.default_attrs
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Size {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn redraw(&self) -> bool {
|
||||
self.buffer.redraw()
|
||||
}
|
||||
|
||||
pub fn set_redraw(&mut self, redraw: bool) {
|
||||
self.with_buffer_mut(|buffer| buffer.set_redraw(redraw));
|
||||
}
|
||||
|
||||
pub fn input<I: Into<Cow<'static, [u8]>>>(&self, input: I) {
|
||||
self.notifier.notify(input);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32, scale_factor: f32) {
|
||||
//TODO: check scale factor
|
||||
if width != self.size.width || height != self.size.height {
|
||||
let instant = Instant::now();
|
||||
|
||||
self.size.width = width;
|
||||
self.size.height = height;
|
||||
|
||||
self.notifier.on_resize(self.size.into());
|
||||
self.term.lock().resize(self.size);
|
||||
|
||||
let metrics = self.metrics.scale(scale_factor);
|
||||
self.with_buffer_mut(|buffer| {
|
||||
let mut font_system = font_system().write().unwrap();
|
||||
buffer.set_metrics_and_size(
|
||||
font_system.raw(),
|
||||
metrics,
|
||||
(width as f32) * scale_factor,
|
||||
(height as f32) * scale_factor,
|
||||
);
|
||||
});
|
||||
|
||||
log::debug!("resize {:?}", instant.elapsed());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self) -> bool {
|
||||
let instant = Instant::now();
|
||||
|
||||
//TODO: is redraw needed after all events?
|
||||
//TODO: use LineDamageBounds
|
||||
{
|
||||
let mut buffer = Arc::make_mut(&mut self.buffer);
|
||||
|
||||
let mut last_point = Point::new(Line(0), Column(0));
|
||||
let mut text = String::new();
|
||||
let mut attrs_list = AttrsList::new(self.default_attrs);
|
||||
{
|
||||
let term_guard = self.term.lock();
|
||||
let grid = term_guard.grid();
|
||||
for indexed in grid.display_iter() {
|
||||
if indexed.point.line != last_point.line {
|
||||
let line_i = last_point.line.0 as usize;
|
||||
while line_i >= buffer.lines.len() {
|
||||
buffer.lines.push(BufferLine::new(
|
||||
"",
|
||||
AttrsList::new(self.default_attrs),
|
||||
Shaping::Advanced,
|
||||
));
|
||||
buffer.set_redraw(true);
|
||||
}
|
||||
|
||||
if buffer.lines[line_i].set_text(text.clone(), attrs_list.clone()) {
|
||||
buffer.set_redraw(true);
|
||||
}
|
||||
|
||||
text.clear();
|
||||
attrs_list.clear_spans();
|
||||
}
|
||||
//TODO: use indexed.point.column?
|
||||
|
||||
//TODO: skip leading spacer?
|
||||
if indexed.cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
|
||||
// Skip wide spacers (cells after wide characters)
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = text.len();
|
||||
text.push(indexed.cell.c);
|
||||
if let Some(zerowidth) = indexed.cell.zerowidth() {
|
||||
for &c in zerowidth {
|
||||
text.push(c);
|
||||
}
|
||||
}
|
||||
let end = text.len();
|
||||
|
||||
let mut attrs = self.default_attrs;
|
||||
let mut fg = convert_color(&self.colors, indexed.cell.fg);
|
||||
let mut bg = convert_color(&self.colors, indexed.cell.bg);
|
||||
//TODO: better handling of cursor
|
||||
if indexed.point == grid.cursor.point {
|
||||
mem::swap(&mut fg, &mut bg);
|
||||
}
|
||||
attrs = attrs.color(fg);
|
||||
// Use metadata as background color
|
||||
attrs = attrs.metadata(bg.0 as usize);
|
||||
//TODO: more flags
|
||||
if indexed.cell.flags.contains(Flags::BOLD) {
|
||||
attrs = attrs.weight(Weight::BOLD);
|
||||
}
|
||||
if indexed.cell.flags.contains(Flags::ITALIC) {
|
||||
attrs = attrs.style(Style::Italic);
|
||||
}
|
||||
if attrs != attrs_list.defaults() {
|
||||
attrs_list.add_span(start..end, attrs);
|
||||
}
|
||||
|
||||
last_point = indexed.point;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: do not repeat!
|
||||
let line_i = last_point.line.0 as usize;
|
||||
while line_i >= buffer.lines.len() {
|
||||
buffer.lines.push(BufferLine::new(
|
||||
"",
|
||||
AttrsList::new(self.default_attrs),
|
||||
Shaping::Advanced,
|
||||
));
|
||||
buffer.set_redraw(true);
|
||||
}
|
||||
|
||||
if buffer.lines[line_i].set_text(text, attrs_list) {
|
||||
buffer.set_redraw(true);
|
||||
}
|
||||
|
||||
if buffer.lines.len() != line_i + 1 {
|
||||
buffer.lines.truncate(line_i + 1);
|
||||
buffer.set_redraw(true);
|
||||
}
|
||||
|
||||
{
|
||||
let mut font_system = font_system().write().unwrap();
|
||||
buffer.shape_until_scroll(font_system.raw(), true);
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("buffer update {:?}", instant.elapsed());
|
||||
|
||||
self.buffer.redraw()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Terminal {
|
||||
fn drop(&mut self) {
|
||||
// Ensure shutdown on terminal drop
|
||||
let _ = self.notifier.0.send(Msg::Shutdown);
|
||||
}
|
||||
}
|
||||
617
src/terminal_box.rs
Normal file
617
src/terminal_box.rs
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{
|
||||
iced::{
|
||||
advanced::graphics::text::{font_system, Raw},
|
||||
event::{Event, Status},
|
||||
keyboard::{Event as KeyEvent, KeyCode, Modifiers},
|
||||
mouse::{self, Button, Event as MouseEvent, ScrollDelta},
|
||||
Color, Element, Length, Padding, Point, Rectangle, Size, Vector,
|
||||
},
|
||||
iced_core::{
|
||||
clipboard::Clipboard,
|
||||
image,
|
||||
layout::{self, Layout},
|
||||
renderer::{self, Quad},
|
||||
text,
|
||||
widget::{self, tree, Widget},
|
||||
Shell,
|
||||
},
|
||||
};
|
||||
use cosmic_text::{Action, Edit, Metrics, Motion, Scroll};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
cmp,
|
||||
sync::Mutex,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::Terminal;
|
||||
|
||||
pub struct TerminalBox<'a, Message> {
|
||||
terminal: &'a Mutex<Terminal>,
|
||||
padding: Padding,
|
||||
click_timing: Duration,
|
||||
context_menu: Option<Point>,
|
||||
on_context_menu: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
|
||||
}
|
||||
|
||||
impl<'a, Message> TerminalBox<'a, Message>
|
||||
where
|
||||
Message: Clone,
|
||||
{
|
||||
pub fn new(terminal: &'a Mutex<Terminal>) -> Self {
|
||||
Self {
|
||||
terminal,
|
||||
padding: Padding::new(0.0),
|
||||
click_timing: Duration::from_millis(500),
|
||||
context_menu: None,
|
||||
on_context_menu: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn click_timing(mut self, click_timing: Duration) -> Self {
|
||||
self.click_timing = click_timing;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn context_menu(mut self, position: Point) -> Self {
|
||||
self.context_menu = Some(position);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_context_menu(
|
||||
mut self,
|
||||
on_context_menu: impl Fn(Option<Point>) -> Message + 'a,
|
||||
) -> Self {
|
||||
self.on_context_menu = Some(Box::new(on_context_menu));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn terminal_box<'a, Message>(terminal: &'a Mutex<Terminal>) -> TerminalBox<'a, Message>
|
||||
where
|
||||
Message: Clone,
|
||||
{
|
||||
TerminalBox::new(terminal)
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for TerminalBox<'a, Message>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer:
|
||||
renderer::Renderer + image::Renderer<Handle = image::Handle> + text::Renderer<Raw = Raw>,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Fill
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_tree: &mut widget::Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(Length::Fill).height(Length::Fill);
|
||||
|
||||
let mut terminal = self.terminal.lock().unwrap();
|
||||
//TODO: set size?
|
||||
terminal.with_buffer_mut(|buffer| {
|
||||
let mut font_system = font_system().write().unwrap();
|
||||
buffer.shape_until_scroll(font_system.raw(), true);
|
||||
});
|
||||
|
||||
terminal.with_buffer(|buffer| {
|
||||
let mut layout_lines = 0;
|
||||
for line in buffer.lines.iter() {
|
||||
match line.layout_opt() {
|
||||
Some(layout) => layout_lines += layout.len(),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
let height = layout_lines as f32 * buffer.metrics().line_height;
|
||||
let size = Size::new(limits.max().width, height);
|
||||
|
||||
layout::Node::new(limits.resolve(size))
|
||||
})
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
match &state.dragging {
|
||||
Some(Dragging::Scrollbar { .. }) => return mouse::Interaction::Idle,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
||||
let scale_factor = state.scale_factor.get();
|
||||
let terminal = self.terminal.lock().unwrap();
|
||||
let buffer_size = terminal.with_buffer(|buffer| buffer.size());
|
||||
|
||||
let x_logical = p.x - self.padding.left;
|
||||
let y_logical = p.y - self.padding.top;
|
||||
let x = x_logical * scale_factor;
|
||||
let y = y_logical * scale_factor;
|
||||
if x >= 0.0 && x < buffer_size.0 && y >= 0.0 && y < buffer_size.1 {
|
||||
return mouse::Interaction::Text;
|
||||
}
|
||||
}
|
||||
|
||||
mouse::Interaction::Idle
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &Renderer::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let instant = Instant::now();
|
||||
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
let mut terminal = self.terminal.lock().unwrap();
|
||||
|
||||
//TODO: make this configurable
|
||||
let scrollbar_w = 0.0;
|
||||
|
||||
let view_position =
|
||||
layout.position() + [self.padding.left as f32, self.padding.top as f32].into();
|
||||
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32)
|
||||
- self.padding.horizontal() as i32;
|
||||
let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32)
|
||||
- self.padding.vertical() as i32
|
||||
- scrollbar_w as i32;
|
||||
let scale_factor = style.scale_factor as f32;
|
||||
|
||||
if view_w <= 0 || view_h <= 0 {
|
||||
// Zero sized image
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure terminal is the right size
|
||||
terminal.resize(view_w as u32, view_h as u32, scale_factor);
|
||||
|
||||
// Cache scale factor
|
||||
state.scale_factor.set(scale_factor);
|
||||
|
||||
// Ensure terminal is shaped
|
||||
terminal.with_buffer_mut(|buffer| {
|
||||
let mut font_system = font_system().write().unwrap();
|
||||
buffer.shape_until_scroll(font_system.raw(), true);
|
||||
});
|
||||
|
||||
// Render default background
|
||||
{
|
||||
let background_color = cosmic_text::Color(terminal.default_attrs().metadata as u32);
|
||||
renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: Rectangle::new(view_position, Size::new(view_w as f32, view_h as f32)),
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Color::new(
|
||||
background_color.r() as f32 / 255.0,
|
||||
background_color.g() as f32 / 255.0,
|
||||
background_color.b() as f32 / 255.0,
|
||||
background_color.a() as f32 / 255.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Render cell backgrounds that do not match default
|
||||
terminal.with_buffer(|buffer| {
|
||||
let line_height = buffer.metrics().line_height;
|
||||
for run in buffer.layout_runs() {
|
||||
for glyph in run.glyphs.iter() {
|
||||
if glyph.metadata != terminal.default_attrs().metadata {
|
||||
let background_color = cosmic_text::Color(glyph.metadata as u32);
|
||||
renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: Rectangle::new(
|
||||
view_position + Vector::new(glyph.x, run.line_top),
|
||||
Size::new(glyph.w, line_height),
|
||||
),
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Color::new(
|
||||
background_color.r() as f32 / 255.0,
|
||||
background_color.g() as f32 / 255.0,
|
||||
background_color.b() as f32 / 255.0,
|
||||
background_color.a() as f32 / 255.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
renderer.fill_raw(Raw {
|
||||
buffer: terminal.buffer_weak(),
|
||||
position: view_position,
|
||||
color: Color::new(1.0, 1.0, 1.0, 1.0), // TODO
|
||||
clip_bounds: Rectangle::new(view_position, Size::new(view_w as f32, view_h as f32)),
|
||||
});
|
||||
|
||||
/*TODO
|
||||
// Draw scrollbar
|
||||
let scrollbar_alpha = match &state.dragging {
|
||||
Some(Dragging::Scrollbar { .. }) => 0.5,
|
||||
_ => 0.25,
|
||||
};
|
||||
renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: state.scrollbar_rect.get() + Vector::new(view_position.x, view_position.y),
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Color::new(1.0, 1.0, 1.0, scrollbar_alpha),
|
||||
);
|
||||
*/
|
||||
|
||||
let duration = instant.elapsed();
|
||||
log::debug!("redraw {}, {}: {:?}", view_w, view_h, duration);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle<f32>,
|
||||
) -> Status {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let scale_factor = state.scale_factor.get();
|
||||
let scrollbar_rect = state.scrollbar_rect.get();
|
||||
let mut terminal = self.terminal.lock().unwrap();
|
||||
let buffer_size = terminal.with_buffer(|buffer| buffer.size());
|
||||
let mut font_system = font_system().write().unwrap();
|
||||
|
||||
let mut status = Status::Ignored;
|
||||
match event {
|
||||
//TODO: Alt keys when they are control characters
|
||||
Event::Keyboard(KeyEvent::KeyPressed {
|
||||
key_code,
|
||||
modifiers,
|
||||
}) => match key_code {
|
||||
KeyCode::Backspace => {
|
||||
terminal.input(b"\x08".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
if modifiers.shift() {
|
||||
terminal.input(b"\x1B[Z".as_slice());
|
||||
} else {
|
||||
terminal.input(b"\t".as_slice());
|
||||
}
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
terminal.input(b"\n".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Escape => {
|
||||
terminal.input(b"\x1B".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
terminal.input(b"\x1B[A".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
terminal.input(b"\x1B[B".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
terminal.input(b"\x1B[C".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
terminal.input(b"\x1B[D".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::End => {
|
||||
terminal.input(b"\x1B[F".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Home => {
|
||||
terminal.input(b"\x1B[H".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Insert => {
|
||||
terminal.input(b"\x1B[2~".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
terminal.input(b"\x1B[3~".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
terminal.input(b"\x1B[5~".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
terminal.input(b"\x1B[6~".as_slice());
|
||||
status = Status::Captured;
|
||||
}
|
||||
//TODO: F1-F12 keys
|
||||
_ => (),
|
||||
},
|
||||
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
||||
state.modifiers = modifiers;
|
||||
}
|
||||
Event::Keyboard(KeyEvent::CharacterReceived(character)) => {
|
||||
match (
|
||||
state.modifiers.logo(),
|
||||
state.modifiers.control(),
|
||||
state.modifiers.alt(),
|
||||
) {
|
||||
(true, _, _) => {
|
||||
// Ignore super
|
||||
}
|
||||
(false, true, _) => {
|
||||
// Handle ctrl for control characters (Ctrl-A to Ctrl-Z)
|
||||
if character.is_control() {
|
||||
let mut buf = [0, 0, 0, 0];
|
||||
let str = character.encode_utf8(&mut buf);
|
||||
terminal.input(str.as_bytes().to_vec());
|
||||
}
|
||||
}
|
||||
(false, false, true) => {
|
||||
if !character.is_control() {
|
||||
// Handle alt for non-control characters
|
||||
let mut buf = [0x1B, 0, 0, 0, 0];
|
||||
let str = character.encode_utf8(&mut buf[1..]);
|
||||
terminal.input(str.as_bytes().to_vec());
|
||||
}
|
||||
}
|
||||
(false, false, false) => {
|
||||
// Handle no modifiers for non-control characters
|
||||
if !character.is_control() {
|
||||
let mut buf = [0, 0, 0, 0];
|
||||
let str = character.encode_utf8(&mut buf);
|
||||
terminal.input(str.as_bytes().to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
status = Status::Captured;
|
||||
}
|
||||
/*TODO
|
||||
Event::Mouse(MouseEvent::ButtonPressed(button)) => {
|
||||
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
||||
// Handle left click drag
|
||||
if let Button::Left = button {
|
||||
let x_logical = p.x - self.padding.left;
|
||||
let y_logical = p.y - self.padding.top;
|
||||
let x = x_logical * scale_factor;
|
||||
let y = y_logical * scale_factor;
|
||||
if x >= 0.0 && x < buffer_size.0 && y >= 0.0 && y < buffer_size.1 {
|
||||
let click_kind =
|
||||
if let Some((click_kind, click_time)) = state.click.take() {
|
||||
if click_time.elapsed() < self.click_timing {
|
||||
match click_kind {
|
||||
ClickKind::Single => ClickKind::Double,
|
||||
ClickKind::Double => ClickKind::Triple,
|
||||
ClickKind::Triple => ClickKind::Single,
|
||||
}
|
||||
} else {
|
||||
ClickKind::Single
|
||||
}
|
||||
} else {
|
||||
ClickKind::Single
|
||||
};
|
||||
match click_kind {
|
||||
ClickKind::Single => editor.action(Action::Click {
|
||||
x: x as i32,
|
||||
y: y as i32,
|
||||
}),
|
||||
ClickKind::Double => editor.action(Action::DoubleClick {
|
||||
x: x as i32,
|
||||
y: y as i32,
|
||||
}),
|
||||
ClickKind::Triple => editor.action(Action::TripleClick {
|
||||
x: x as i32,
|
||||
y: y as i32,
|
||||
}),
|
||||
}
|
||||
state.click = Some((click_kind, Instant::now()));
|
||||
state.dragging = Some(Dragging::Buffer);
|
||||
} else if scrollbar_rect.contains(Point::new(x_logical, y_logical)) {
|
||||
state.dragging = Some(Dragging::Scrollbar {
|
||||
start_y: y,
|
||||
start_scroll: editor.with_buffer(|buffer| buffer.scroll()),
|
||||
});
|
||||
} else if x_logical >= scrollbar_rect.x
|
||||
&& x_logical < (scrollbar_rect.x + scrollbar_rect.width)
|
||||
{
|
||||
editor.with_buffer_mut(|buffer| {
|
||||
let scroll_line =
|
||||
((y / buffer.size().1) * buffer.lines.len() as f32) as i32;
|
||||
buffer.set_scroll(Scroll::new(
|
||||
scroll_line.try_into().unwrap_or_default(),
|
||||
0,
|
||||
));
|
||||
state.dragging = Some(Dragging::Scrollbar {
|
||||
start_y: y,
|
||||
start_scroll: buffer.scroll(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update context menu state
|
||||
if let Some(on_context_menu) = &self.on_context_menu {
|
||||
shell.publish((on_context_menu)(match self.context_menu {
|
||||
Some(_) => None,
|
||||
None => match button {
|
||||
Button::Right => Some(p),
|
||||
_ => None,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
status = Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => {
|
||||
state.dragging = None;
|
||||
status = Status::Captured;
|
||||
}
|
||||
Event::Mouse(MouseEvent::CursorMoved { .. }) => {
|
||||
if let Some(dragging) = &state.dragging {
|
||||
if let Some(p) = cursor_position.position() {
|
||||
let x_logical = (p.x - layout.bounds().x) - self.padding.left;
|
||||
let y_logical = (p.y - layout.bounds().y) - self.padding.top;
|
||||
let x = x_logical * scale_factor;
|
||||
let y = y_logical * scale_factor;
|
||||
match dragging {
|
||||
Dragging::Buffer => {
|
||||
editor.action(Action::Drag {
|
||||
x: x as i32,
|
||||
y: y as i32,
|
||||
});
|
||||
}
|
||||
Dragging::Scrollbar {
|
||||
start_y,
|
||||
start_scroll,
|
||||
} => {
|
||||
editor.with_buffer_mut(|buffer| {
|
||||
let scroll_offset = (((y - start_y) / buffer.size().1)
|
||||
* buffer.lines.len() as f32)
|
||||
as i32;
|
||||
buffer.set_scroll(Scroll::new(
|
||||
(start_scroll.line as i32 + scroll_offset)
|
||||
.try_into()
|
||||
.unwrap_or_default(),
|
||||
0,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
status = Status::Captured;
|
||||
}
|
||||
}
|
||||
Event::Mouse(MouseEvent::WheelScrolled { delta }) => {
|
||||
if let Some(_p) = cursor_position.position_in(layout.bounds()) {
|
||||
match delta {
|
||||
ScrollDelta::Lines { x, y } => {
|
||||
//TODO: this adjustment is just a guess!
|
||||
state.scroll_pixels = 0.0;
|
||||
let lines = (-y * 6.0) as i32;
|
||||
if lines != 0 {
|
||||
editor.action(Action::Scroll { lines });
|
||||
}
|
||||
status = Status::Captured;
|
||||
}
|
||||
ScrollDelta::Pixels { x, y } => {
|
||||
//TODO: this adjustment is just a guess!
|
||||
state.scroll_pixels -= y * 6.0;
|
||||
let mut lines = 0;
|
||||
let metrics = editor.with_buffer(|buffer| buffer.metrics());
|
||||
while state.scroll_pixels <= -metrics.line_height {
|
||||
lines -= 1;
|
||||
state.scroll_pixels += metrics.line_height;
|
||||
}
|
||||
while state.scroll_pixels >= metrics.line_height {
|
||||
lines += 1;
|
||||
state.scroll_pixels -= metrics.line_height;
|
||||
}
|
||||
if lines != 0 {
|
||||
editor.action(Action::Scroll { lines });
|
||||
}
|
||||
status = Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
_ => (),
|
||||
}
|
||||
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<TerminalBox<'a, Message>> for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: Clone + 'a,
|
||||
Renderer:
|
||||
renderer::Renderer + image::Renderer<Handle = image::Handle> + text::Renderer<Raw = Raw>,
|
||||
{
|
||||
fn from(terminal_box: TerminalBox<'a, Message>) -> Self {
|
||||
Self::new(terminal_box)
|
||||
}
|
||||
}
|
||||
|
||||
enum ClickKind {
|
||||
Single,
|
||||
Double,
|
||||
Triple,
|
||||
}
|
||||
|
||||
enum Dragging {
|
||||
Buffer,
|
||||
Scrollbar { start_y: f32, start_scroll: Scroll },
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
modifiers: Modifiers,
|
||||
click: Option<(ClickKind, Instant)>,
|
||||
dragging: Option<Dragging>,
|
||||
scale_factor: Cell<f32>,
|
||||
scroll_pixels: f32,
|
||||
scrollbar_rect: Cell<Rectangle<f32>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State {
|
||||
modifiers: Modifiers::empty(),
|
||||
click: None,
|
||||
dragging: None,
|
||||
scale_factor: Cell::new(1.0),
|
||||
scroll_pixels: 0.0,
|
||||
scrollbar_rect: Cell::new(Rectangle::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue