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]
|
[dependencies]
|
||||||
alacritty_terminal = "0.19"
|
alacritty_terminal = "0.19"
|
||||||
bytemuck = "1.14"
|
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tiny-skia = "0.11"
|
tokio = { version = "1.35", features = ["sync"] }
|
||||||
|
|
||||||
[dependencies.cosmic-text]
|
[dependencies.cosmic-text]
|
||||||
git = "https://github.com/pop-os/cosmic-text.git"
|
git = "https://github.com/pop-os/cosmic-text.git"
|
||||||
branch = "refactor"
|
branch = "refactor"
|
||||||
|
|
||||||
[dependencies.glyphon]
|
[dependencies.libcosmic]
|
||||||
git = "https://github.com/jackpot51/glyphon.git"
|
git = "https://github.com/pop-os/libcosmic.git"
|
||||||
branch = "refactor"
|
default-features = false
|
||||||
optional = true
|
features = ["tokio", "winit"]
|
||||||
|
|
||||||
[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"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["wgpu"]
|
||||||
wgpu = ["dep:glyphon", "dep:pollster", "dep:wgpu"]
|
wgpu = ["libcosmic/wgpu"]
|
||||||
|
|
|
||||||
690
src/main.rs
690
src/main.rs
|
|
@ -1,430 +1,282 @@
|
||||||
use alacritty_terminal::{
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
ansi::{Color, NamedColor},
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
config::{Config, PtyConfig},
|
|
||||||
event::{Event as TermEvent, EventListener, Notify, OnResize, WindowSize},
|
use alacritty_terminal::event::Event as TermEvent;
|
||||||
event_loop::{EventLoop as PtyEventLoop, Notifier},
|
use cosmic::{
|
||||||
grid::Dimensions,
|
app::{Command, Core, Settings},
|
||||||
index::{Column, Line, Point},
|
cosmic_theme, executor,
|
||||||
sync::FairMutex,
|
iced::{
|
||||||
term::{
|
self,
|
||||||
cell::Flags,
|
futures::SinkExt,
|
||||||
color::{Colors, Rgb},
|
subscription::{self, Subscription},
|
||||||
|
widget::row,
|
||||||
|
window, Alignment, Length,
|
||||||
},
|
},
|
||||||
tty, Term,
|
iced_core::Size,
|
||||||
};
|
style,
|
||||||
use cosmic_text::{
|
widget::{self, segmented_button},
|
||||||
Attrs, AttrsList, Buffer, BufferLine, Family, FontSystem, Metrics, Shaping, Style, SwashCache,
|
ApplicationExt, Element,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
use std::{any::TypeId, sync::Mutex};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use self::renderer::Renderer;
|
use self::terminal::Terminal;
|
||||||
mod renderer;
|
mod terminal;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
use self::terminal_box::terminal_box;
|
||||||
struct Size {
|
mod terminal_box;
|
||||||
width: f32,
|
|
||||||
height: f32,
|
|
||||||
cell_width: f32,
|
|
||||||
cell_height: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dimensions for Size {
|
/// Runs application with these settings
|
||||||
fn total_lines(&self) -> usize {
|
#[rustfmt::skip]
|
||||||
self.screen_lines()
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
env_logger::init();
|
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 settings = Settings::default()
|
||||||
let event_loop_proxy = event_loop.create_proxy();
|
.antialiasing(true)
|
||||||
let window = Rc::new(WindowBuilder::new().build(&event_loop).unwrap());
|
.client_decorations(true)
|
||||||
let (width, height) = {
|
.debug(false)
|
||||||
let inner_size = window.inner_size();
|
.default_icon_theme("Pop")
|
||||||
(inner_size.width as f32, inner_size.height as f32)
|
.default_text_size(16.0)
|
||||||
};
|
.scale_factor(1.0)
|
||||||
|
.size(Size::new(1024., 768.))
|
||||||
|
.theme(cosmic::Theme::dark());
|
||||||
|
|
||||||
let config = Config::default();
|
cosmic::app::run::<App>(settings, ())?;
|
||||||
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 pty_config = PtyConfig::default();
|
Ok(())
|
||||||
let window_id = 0;
|
}
|
||||||
let pty = tty::new(&pty_config, dimensions.into(), window_id).unwrap();
|
|
||||||
|
/// Messages that are used specifically by our [`App`].
|
||||||
let pty_event_loop = PtyEventLoop::new(term.clone(), event_proxy, pty, pty_config.hold, false);
|
#[derive(Clone, Debug)]
|
||||||
let mut notifier = Notifier(pty_event_loop.channel());
|
pub enum Message {
|
||||||
let pty_join_handle = pty_event_loop.spawn();
|
TabActivate(segmented_button::Entity),
|
||||||
|
TabClose(segmented_button::Entity),
|
||||||
let mut renderer = Renderer::new(window.clone()).unwrap();
|
TabNew,
|
||||||
let mut modifiers = ModifiersState::default();
|
TermEvent(segmented_button::Entity, TermEvent),
|
||||||
event_loop.run(move |event, _elwt, control_flow| {
|
TermEventTx(mpsc::Sender<(segmented_button::Entity, TermEvent)>),
|
||||||
*control_flow = ControlFlow::Wait;
|
}
|
||||||
|
|
||||||
match event {
|
/// The [`App`] stores application-specific state.
|
||||||
WinitEvent::RedrawRequested(window_id) if window_id == window.id() => {
|
pub struct App {
|
||||||
let instant = Instant::now();
|
core: Core,
|
||||||
|
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
|
||||||
renderer.render(&mut buffer, &mut font_system, &mut swash_cache);
|
term_event_tx_opt: Option<mpsc::Sender<(segmented_button::Entity, TermEvent)>>,
|
||||||
|
}
|
||||||
println!("draw {:?}", instant.elapsed());
|
|
||||||
}
|
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
||||||
WinitEvent::WindowEvent {
|
impl cosmic::Application for App {
|
||||||
event: WindowEvent::ReceivedCharacter(c),
|
/// Default async executor to use with the app.
|
||||||
window_id,
|
type Executor = executor::Default;
|
||||||
} if window_id == window.id() => {
|
|
||||||
match (modifiers.logo(), modifiers.ctrl(), modifiers.alt()) {
|
/// Argument received [`cosmic::Application::new`].
|
||||||
(true, _, _) => {} // Ignore super
|
type Flags = ();
|
||||||
(false, _, true) => {
|
|
||||||
// Alt keys
|
/// Message type specific to our [`App`].
|
||||||
let mut buf = [0x1B, 0, 0, 0, 0];
|
type Message = Message;
|
||||||
let str = c.encode_utf8(&mut buf[1..]);
|
|
||||||
notifier.notify(str.as_bytes().to_vec());
|
/// The unique application ID to supply to the window manager.
|
||||||
}
|
const APP_ID: &'static str = "org.cosmic.AppDemo";
|
||||||
(false, _, false) => {
|
|
||||||
let mut buf = [0, 0, 0, 0];
|
fn core(&self) -> &Core {
|
||||||
let str = c.encode_utf8(&mut buf);
|
&self.core
|
||||||
notifier.notify(str.as_bytes().to_vec());
|
}
|
||||||
}
|
|
||||||
}
|
fn core_mut(&mut self) -> &mut Core {
|
||||||
}
|
&mut self.core
|
||||||
WinitEvent::WindowEvent {
|
}
|
||||||
event:
|
|
||||||
WindowEvent::KeyboardInput {
|
/// Creates the application, and optionally emits command on initialize.
|
||||||
input:
|
fn init(core: Core, input: Self::Flags) -> (Self, Command<Self::Message>) {
|
||||||
KeyboardInput {
|
let mut app = App {
|
||||||
state: ElementState::Pressed,
|
core,
|
||||||
virtual_keycode: Some(virtual_keycode),
|
tab_model: segmented_button::ModelBuilder::default().build(),
|
||||||
..
|
term_event_tx_opt: None,
|
||||||
},
|
};
|
||||||
..
|
|
||||||
},
|
let command = app.update_title();
|
||||||
window_id,
|
|
||||||
} if window_id == window.id() => {
|
(app, command)
|
||||||
match (modifiers.logo(), modifiers.ctrl(), modifiers.alt()) {
|
}
|
||||||
(true, _, _) => {} // Ignore super
|
|
||||||
(false, true, _) => {
|
/// Handle application events here.
|
||||||
// Control keys will use ReceivedCharacter instead
|
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||||
}
|
match message {
|
||||||
(false, false, true) => {
|
Message::TabActivate(entity) => {
|
||||||
//TODO: support Alt keys without character
|
self.tab_model.activate(entity);
|
||||||
}
|
return self.update_title();
|
||||||
(false, false, false) => match virtual_keycode {
|
}
|
||||||
VirtualKeyCode::Up => {
|
Message::TabClose(entity) => {
|
||||||
notifier.notify(b"\x1B[A".as_slice());
|
// Activate closest item
|
||||||
}
|
if let Some(position) = self.tab_model.position(entity) {
|
||||||
VirtualKeyCode::Down => {
|
if position > 0 {
|
||||||
notifier.notify(b"\x1B[B".as_slice());
|
self.tab_model.activate_position(position - 1);
|
||||||
}
|
} else {
|
||||||
VirtualKeyCode::Right => {
|
self.tab_model.activate_position(position + 1);
|
||||||
notifier.notify(b"\x1B[C".as_slice());
|
}
|
||||||
}
|
}
|
||||||
VirtualKeyCode::Left => {
|
|
||||||
notifier.notify(b"\x1B[D".as_slice());
|
// Remove item
|
||||||
}
|
self.tab_model.remove(entity);
|
||||||
VirtualKeyCode::End => {
|
|
||||||
notifier.notify(b"\x1B[F".as_slice());
|
// If that was the last tab, close window
|
||||||
}
|
if self.tab_model.iter().next().is_none() {
|
||||||
VirtualKeyCode::Home => {
|
return window::close(window::Id::MAIN);
|
||||||
notifier.notify(b"\x1B[H".as_slice());
|
}
|
||||||
}
|
|
||||||
_ => {}
|
return self.update_title();
|
||||||
},
|
}
|
||||||
}
|
Message::TabNew => match &self.term_event_tx_opt {
|
||||||
}
|
Some(term_event_tx) => {
|
||||||
WinitEvent::WindowEvent {
|
let entity = self
|
||||||
event: WindowEvent::ModifiersChanged(new_modifiers),
|
.tab_model
|
||||||
window_id,
|
.insert()
|
||||||
} if window_id == window.id() => {
|
.text("New Terminal")
|
||||||
modifiers = new_modifiers;
|
.closable()
|
||||||
}
|
.activate()
|
||||||
WinitEvent::WindowEvent {
|
.id();
|
||||||
event: WindowEvent::Resized(physical_size),
|
let terminal = Terminal::new(entity, term_event_tx.clone());
|
||||||
window_id,
|
self.tab_model
|
||||||
} if window_id == window.id() => {
|
.data_set::<Mutex<Terminal>>(entity, Mutex::new(terminal));
|
||||||
let instant = Instant::now();
|
}
|
||||||
|
None => {
|
||||||
dimensions.width = physical_size.width as f32;
|
log::warn!("tried to create new tab before having event channel");
|
||||||
dimensions.height = physical_size.height as f32;
|
}
|
||||||
|
},
|
||||||
notifier.on_resize(dimensions.into());
|
Message::TermEvent(entity, event) => match event {
|
||||||
|
TermEvent::Bell => {
|
||||||
term.lock().resize(dimensions);
|
//TODO: audible or visible bell options?
|
||||||
|
}
|
||||||
buffer.set_size(
|
TermEvent::ColorRequest(index, f) => {
|
||||||
&mut font_system,
|
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
||||||
dimensions.width as f32,
|
let terminal = terminal.lock().unwrap();
|
||||||
dimensions.height as f32,
|
let rgb = terminal.colors()[index].unwrap_or_default();
|
||||||
);
|
let text = f(rgb);
|
||||||
|
terminal.input(text.into_bytes());
|
||||||
renderer.resize(physical_size.width, physical_size.height);
|
}
|
||||||
|
}
|
||||||
println!("resize {:?}", instant.elapsed());
|
TermEvent::Exit => {
|
||||||
}
|
return self.update(Message::TabClose(entity));
|
||||||
WinitEvent::WindowEvent {
|
}
|
||||||
event: WindowEvent::CloseRequested,
|
TermEvent::PtyWrite(text) => {
|
||||||
window_id,
|
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
||||||
} if window_id == window.id() => {
|
let terminal = terminal.lock().unwrap();
|
||||||
term.lock().exit();
|
terminal.input(text.into_bytes());
|
||||||
}
|
}
|
||||||
WinitEvent::UserEvent(user_event) => {
|
}
|
||||||
println!("{:?}", user_event);
|
TermEvent::ResetTitle => {
|
||||||
match user_event {
|
self.tab_model.text_set(entity, "New Terminal");
|
||||||
//TODO: other error codes?
|
return self.update_title();
|
||||||
TermEvent::Exit => *control_flow = ControlFlow::ExitWithCode(0),
|
}
|
||||||
TermEvent::PtyWrite(text) => notifier.notify(text.into_bytes()),
|
TermEvent::TextAreaSizeRequest(f) => {
|
||||||
TermEvent::Title(title) => {
|
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
||||||
window.set_title(&title);
|
let terminal = terminal.lock().unwrap();
|
||||||
}
|
let text = f(terminal.size().into());
|
||||||
_ => {}
|
terminal.input(text.into_bytes());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let instant = Instant::now();
|
TermEvent::Title(title) => {
|
||||||
|
self.tab_model.text_set(entity, title);
|
||||||
//TODO: is redraw needed after all events?
|
return self.update_title();
|
||||||
//TODO: use LineDamageBounds
|
}
|
||||||
{
|
TermEvent::Wakeup => {
|
||||||
let mut last_point = Point::new(Line(0), Column(0));
|
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
||||||
let mut text = String::new();
|
let mut terminal = terminal.lock().unwrap();
|
||||||
let mut attrs_list = AttrsList::new(default_attrs);
|
terminal.update();
|
||||||
let term_guard = term.lock();
|
}
|
||||||
let grid = term_guard.grid();
|
}
|
||||||
for indexed in grid.display_iter() {
|
_ => {
|
||||||
if indexed.point.line != last_point.line {
|
println!("TODO: {:?}", event);
|
||||||
let line_i = last_point.line.0 as usize;
|
}
|
||||||
while line_i >= buffer.lines.len() {
|
},
|
||||||
buffer.lines.push(BufferLine::new(
|
Message::TermEventTx(term_event_tx) => {
|
||||||
"",
|
self.term_event_tx_opt = Some(term_event_tx);
|
||||||
AttrsList::new(default_attrs),
|
}
|
||||||
Shaping::Advanced,
|
}
|
||||||
));
|
|
||||||
buffer.set_redraw(true);
|
Command::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer.lines[line_i].set_text(text.clone(), attrs_list.clone()) {
|
/// Creates a view after each update.
|
||||||
buffer.set_redraw(true);
|
fn view(&self) -> Element<Self::Message> {
|
||||||
}
|
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
|
||||||
|
|
||||||
text.clear();
|
let mut tab_column = widget::column::with_capacity(1);
|
||||||
attrs_list.clear_spans();
|
|
||||||
}
|
tab_column = tab_column.push(
|
||||||
//TODO: use indexed.point.column?
|
row![
|
||||||
|
widget::view_switcher::horizontal(&self.tab_model)
|
||||||
//TODO: skip leading spacer?
|
.button_height(32)
|
||||||
if indexed.cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
|
.button_spacing(space_xxs)
|
||||||
// Skip wide spacers (cells after wide characters)
|
.on_activate(Message::TabActivate)
|
||||||
continue;
|
.on_close(Message::TabClose)
|
||||||
}
|
.width(Length::Shrink),
|
||||||
|
widget::button(widget::icon::from_name("list-add-symbolic").size(16).icon())
|
||||||
let start = text.len();
|
.on_press(Message::TabNew)
|
||||||
text.push(indexed.cell.c);
|
.padding(space_xxs)
|
||||||
if let Some(zerowidth) = indexed.cell.zerowidth() {
|
.style(style::Button::Icon)
|
||||||
for &c in zerowidth {
|
]
|
||||||
text.push(c);
|
.align_items(Alignment::Center),
|
||||||
}
|
);
|
||||||
}
|
|
||||||
let end = text.len();
|
match self
|
||||||
|
.tab_model
|
||||||
let convert_color = |color| {
|
.data::<Mutex<Terminal>>(self.tab_model.active())
|
||||||
let rgb = match color {
|
{
|
||||||
Color::Named(named_color) => match colors[named_color] {
|
Some(terminal) => {
|
||||||
Some(rgb) => rgb,
|
//TODO
|
||||||
None => {
|
tab_column = tab_column.push(terminal_box(terminal));
|
||||||
eprintln!("missing named color {:?}", named_color);
|
}
|
||||||
Rgb::default()
|
None => {
|
||||||
}
|
//TODO
|
||||||
},
|
}
|
||||||
Color::Spec(rgb) => rgb,
|
}
|
||||||
Color::Indexed(index) => match colors[index as usize] {
|
|
||||||
Some(rgb) => rgb,
|
let content: Element<_> = tab_column.into();
|
||||||
None => {
|
|
||||||
eprintln!("missing indexed color {}", index);
|
// Uncomment to debug layout:
|
||||||
Rgb::default()
|
//content.explain(cosmic::iced::Color::WHITE)
|
||||||
}
|
content
|
||||||
},
|
}
|
||||||
};
|
|
||||||
cosmic_text::Color::rgb(rgb.r, rgb.g, rgb.b)
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
};
|
struct TerminalEventWorker;
|
||||||
|
subscription::channel(
|
||||||
let mut attrs = default_attrs;
|
TypeId::of::<TerminalEventWorker>(),
|
||||||
let mut fg = convert_color(indexed.cell.fg);
|
100,
|
||||||
let mut bg = convert_color(indexed.cell.bg);
|
|mut output| async move {
|
||||||
//TODO: better handling of cursor
|
let (event_tx, mut event_rx) = mpsc::channel(100);
|
||||||
if indexed.point == grid.cursor.point {
|
output.send(Message::TermEventTx(event_tx)).await.unwrap();
|
||||||
mem::swap(&mut fg, &mut bg);
|
|
||||||
}
|
// Create first terminal tab
|
||||||
attrs = attrs.color(fg);
|
output.send(Message::TabNew).await.unwrap();
|
||||||
// Use metadata as background color
|
|
||||||
attrs = attrs.metadata(bg.0 as usize);
|
while let Some((entity, event)) = event_rx.recv().await {
|
||||||
//TODO: more flags
|
output
|
||||||
if indexed.cell.flags.contains(Flags::BOLD) {
|
.send(Message::TermEvent(entity, event))
|
||||||
attrs = attrs.weight(Weight::BOLD);
|
.await
|
||||||
}
|
.unwrap();
|
||||||
if indexed.cell.flags.contains(Flags::ITALIC) {
|
}
|
||||||
attrs = attrs.style(Style::Italic);
|
|
||||||
}
|
panic!("terminal event channel closed");
|
||||||
if attrs != attrs_list.defaults() {
|
},
|
||||||
attrs_list.add_span(start..end, attrs);
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
last_point = indexed.point;
|
|
||||||
}
|
impl App
|
||||||
|
where
|
||||||
//TODO: do not repeat!
|
Self: cosmic::Application,
|
||||||
let line_i = last_point.line.0 as usize;
|
{
|
||||||
while line_i >= buffer.lines.len() {
|
fn update_title(&mut self) -> Command<Message> {
|
||||||
buffer.lines.push(BufferLine::new(
|
let (header_title, window_title) = match self.tab_model.text(self.tab_model.active()) {
|
||||||
"",
|
Some(tab_title) => (
|
||||||
AttrsList::new(default_attrs),
|
tab_title.to_string(),
|
||||||
Shaping::Advanced,
|
format!("{tab_title} — COSMIC Terminal"),
|
||||||
));
|
),
|
||||||
buffer.set_redraw(true);
|
None => ("No Tab Open".to_string(), "COSMIC Terminal".to_string()),
|
||||||
}
|
};
|
||||||
|
self.set_header_title(header_title);
|
||||||
if buffer.lines[line_i].set_text(text, attrs_list) {
|
self.set_window_title(window_title)
|
||||||
buffer.set_redraw(true);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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