1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-27 02:25:28 +03:00

remove winit/glutin based frontend

This commit is contained in:
Wez Furlong 2019-10-27 22:59:41 -07:00
parent 66f5f6842d
commit 4cb74e68dd
12 changed files with 4 additions and 3039 deletions

View File

@ -25,7 +25,6 @@ pretty_env_logger = "0.3"
failure = "0.1"
failure_derive = "0.1"
freetype = { path = "deps/freetype" }
glium = { optional=true, version = "0.24", default-features = false, features = ["glutin", "icon_loading"]}
image = "0.21"
harfbuzz = { path = "deps/harfbuzz" }
lazy_static = "1.3"
@ -52,7 +51,6 @@ toml = "0.4"
unicode-normalization = "0.1"
unicode-width = "0.1"
varbincode = "0.1"
winit = {version="0.19",optional=true}
window = { path = "window", features=["opengl"]}
zstd = "0.4"
@ -89,10 +87,6 @@ core-foundation = "0.6"
core-graphics = "0.17"
core-text = "13.2"
[features]
default = ["enable-winit"]
enable-winit = ["winit", "glium"]
[workspace]
[profile.release]

View File

@ -1,338 +0,0 @@
use crate::config::Config;
use crate::font::FontConfiguration;
use crate::frontend::glium::window::GliumTerminalWindow;
use crate::frontend::guicommon::window::TerminalWindow;
use crate::frontend::{front_end, FrontEnd};
use crate::mux::tab::Tab;
use crate::mux::window::WindowId as MuxWindowId;
use crate::mux::{Mux, SessionTerminated};
use failure::{bail, Error, Fallible};
use glium;
use glium::glutin::EventsLoopProxy;
use glium::glutin::WindowId;
use log::{debug, error};
use promise::{BasicExecutor, Executor, Future, SpawnFunc};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, SystemTime};
/// The GuiSender is used as a handle that allows sending SpawnFunc
/// instances to be executed on the gui thread.
#[derive(Clone)]
struct GuiSender {
tx: Sender<SpawnFunc>,
proxy: EventsLoopProxy,
}
impl GuiSender {
pub fn send(&self, what: SpawnFunc) -> Result<(), Error> {
if let Err(err) = self.tx.send(what) {
bail!("send failed: {:?}", err);
}
self.proxy.wakeup()?;
Ok(())
}
fn new(proxy: EventsLoopProxy) -> (GuiSender, Receiver<SpawnFunc>) {
let (tx, rx) = mpsc::channel();
(GuiSender { tx, proxy }, rx)
}
}
#[derive(Clone)]
pub struct GlutinGuiExecutor {
tx: GuiSender,
}
impl BasicExecutor for GlutinGuiExecutor {
fn execute(&self, f: SpawnFunc) {
self.tx.send(f).expect("GlutinExecutor execute failed");
}
}
impl Executor for GlutinGuiExecutor {
fn clone_executor(&self) -> Box<dyn Executor> {
Box::new(GlutinGuiExecutor {
tx: self.tx.clone(),
})
}
}
/// This struct holds references to Windows.
/// The primary mapping is from `WindowId` -> `GliumTerminalWindow`.
#[derive(Default)]
struct Windows {
by_id: HashMap<WindowId, GliumTerminalWindow>,
}
/// The `GuiEventLoop` represents the combined gui event processor,
/// and a core for spawning tasks from futures. It acts as the manager
/// for various events and is responsible for driving things forward.
pub struct GuiEventLoop {
pub event_loop: RefCell<glium::glutin::EventsLoop>,
windows: Rc<RefCell<Windows>>,
gui_tx: GuiSender,
gui_rx: Receiver<SpawnFunc>,
tick_rx: Receiver<()>,
}
const TICK_INTERVAL: Duration = Duration::from_millis(50);
const MAX_POLL_LOOP_DURATION: Duration = Duration::from_millis(500);
pub struct GlutinFrontEnd {
event_loop: Rc<GuiEventLoop>,
}
impl GlutinFrontEnd {
pub fn try_new(mux: &Rc<Mux>) -> Result<Rc<dyn FrontEnd>, Error> {
let event_loop = Rc::new(GuiEventLoop::new(mux)?);
Ok(Rc::new(Self { event_loop }))
}
}
impl FrontEnd for GlutinFrontEnd {
fn gui_executor(&self) -> Box<dyn Executor> {
self.event_loop.gui_executor()
}
fn run_forever(&self) -> Result<(), Error> {
// This convoluted run() signature is present because of this issue:
// https://github.com/tomaka/winit/issues/413
let myself = &self.event_loop;
loop {
// Check the window count; if after processing the futures there
// are no windows left, then we are done.
{
let windows = myself.windows.borrow();
if windows.by_id.is_empty() {
debug!("No more windows; done!");
return Ok(());
}
}
myself.run_event_loop()?;
myself.process_gui_exec()?;
myself.process_tick()?;
}
}
fn spawn_new_window(
&self,
config: &Arc<Config>,
fontconfig: &Rc<FontConfiguration>,
tab: &Rc<dyn Tab>,
window_id: MuxWindowId,
) -> Fallible<()> {
let window =
GliumTerminalWindow::new(&self.event_loop, fontconfig, config, tab, window_id)?;
self.event_loop.add_window(window)
}
}
impl GuiEventLoop {
pub fn new(_mux: &Rc<Mux>) -> Result<Self, Error> {
let event_loop = glium::glutin::EventsLoop::new();
let (gui_tx, gui_rx) = GuiSender::new(event_loop.create_proxy());
// The glutin/glium plumbing has no native tick/timer stuff, so
// we implement one using a thread. Nice.
let proxy = event_loop.create_proxy();
let (tick_tx, tick_rx) = mpsc::channel();
thread::spawn(move || loop {
std::thread::sleep(TICK_INTERVAL);
if tick_tx.send(()).is_err() {
return;
}
if proxy.wakeup().is_err() {
return;
}
});
Ok(Self {
gui_rx,
gui_tx,
tick_rx,
event_loop: RefCell::new(event_loop),
windows: Rc::new(RefCell::new(Default::default())),
})
}
fn gui_executor(&self) -> Box<dyn Executor> {
Box::new(GlutinGuiExecutor {
tx: self.gui_tx.clone(),
})
}
pub fn with_window<F: Send + 'static + Fn(&mut dyn TerminalWindow) -> Result<(), Error>>(
&self,
window_id: WindowId,
func: F,
) {
Future::with_executor(
GlutinGuiExecutor {
tx: self.gui_tx.clone(),
},
move || {
let front_end = front_end().expect("to be called on gui thread");
let front_end = front_end
.downcast_ref::<GlutinFrontEnd>()
.expect("front_end to be GlutinFrontEnd");
let mut windows = front_end.event_loop.windows.borrow_mut();
if let Some(window) = windows.by_id.get_mut(&window_id) {
func(window)
} else {
bail!("no such window {:?}", window_id);
}
},
);
}
/// Add a window to the event loop and run it.
pub fn add_window(&self, window: GliumTerminalWindow) -> Result<(), Error> {
let window_id = window.window_id();
let mut windows = self.windows.borrow_mut();
windows.by_id.insert(window_id, window);
Ok(())
}
/// Process a single winit event
fn process_gui_event(
&self,
event: &glium::glutin::Event,
) -> Result<glium::glutin::ControlFlow, Error> {
use glium::glutin::ControlFlow::{Break, Continue};
use glium::glutin::Event;
let result = match *event {
Event::WindowEvent { window_id, .. } => {
let dead = match self.windows.borrow_mut().by_id.get_mut(&window_id) {
Some(window) => match window.dispatch_event(event) {
Ok(_) => None,
Err(err) => match err.downcast_ref::<SessionTerminated>() {
Some(_) => Some(window_id),
_ => return Err(err),
},
},
None => None,
};
if let Some(window_id) = dead {
self.schedule_window_close(window_id)?;
}
Continue
}
Event::Awakened => Break,
_ => Continue,
};
Ok(result)
}
/// Spawns a future that will gracefully shut down the resources associated
/// with the specified window.
fn schedule_window_close(&self, window_id: WindowId) -> Result<(), Error> {
Future::with_executor(
GlutinGuiExecutor {
tx: self.gui_tx.clone(),
},
move || {
let front_end = front_end().expect("to be called on gui thread");
let front_end = front_end
.downcast_ref::<GlutinFrontEnd>()
.expect("front_end to be GlutinFrontEnd");
let mut windows = front_end.event_loop.windows.borrow_mut();
windows.by_id.remove(&window_id);
Ok(())
},
);
Ok(())
}
/// Run through all of the windows and cause them to paint if they need it.
/// This happens ~50ms or so.
fn do_paint(&self) {
for window in &mut self.windows.borrow_mut().by_id.values_mut() {
window.paint_if_needed().unwrap();
}
}
fn process_gui_exec(&self) -> Result<(), Error> {
let start = SystemTime::now();
loop {
match start.elapsed() {
Ok(elapsed) if elapsed > MAX_POLL_LOOP_DURATION => {
return Ok(());
}
Err(_) => {
return Ok(());
}
_ => {}
}
match self.gui_rx.try_recv() {
Ok(func) => func(),
Err(TryRecvError::Empty) => return Ok(()),
Err(err) => bail!("poll_rx disconnected {:?}", err),
}
}
}
fn process_tick(&self) -> Result<(), Error> {
loop {
match self.tick_rx.try_recv() {
Ok(_) => {
self.test_for_child_exit();
self.do_paint();
}
Err(TryRecvError::Empty) => return Ok(()),
Err(err) => bail!("tick_rx disconnected {:?}", err),
}
}
}
fn test_for_child_exit(&self) {
let window_ids: Vec<WindowId> = self
.windows
.borrow_mut()
.by_id
.iter_mut()
.filter_map(|(window_id, window)| {
if window.test_for_child_exit() {
Some(*window_id)
} else {
None
}
})
.collect();
for window_id in window_ids {
self.schedule_window_close(window_id).ok();
}
}
/// Runs the winit event loop. This blocks until a wakeup signal
/// is delivered to the event loop. The `GuiSender` is our way
/// of trigger those wakeups.
fn run_event_loop(&self) -> Result<(), Error> {
let mut event_loop = self.event_loop.borrow_mut();
event_loop.run_forever(|event| {
use glium::glutin::ControlFlow::{Break, Continue};
let result = self.process_gui_event(&event);
match result {
Ok(Continue) => Continue,
Ok(Break) => Break,
Err(err) => {
error!("Error in event loop: {:?}", err);
Break
}
}
});
Ok(())
}
}

View File

@ -1,2 +0,0 @@
pub mod glutinloop;
pub mod window;

View File

@ -1,822 +0,0 @@
//! Generic system dependent windows via glium+glutin
use crate::config::Config;
use crate::font::FontConfiguration;
use crate::frontend::glium::glutinloop::GuiEventLoop;
use crate::frontend::guicommon::host::{HostHelper, HostImpl, TabHost};
use crate::frontend::guicommon::window::{Dimensions, TerminalWindow};
use crate::mux::tab::Tab;
use crate::mux::window::WindowId;
use crate::mux::{Mux, SessionTerminated};
use crate::opengl::render::Renderer;
use failure::{format_err, Error};
use glium;
use glium::glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
use glium::glutin::{self, ElementState, MouseCursor};
use log::{debug, error};
use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant};
use term;
use term::KeyCode;
use term::KeyModifiers;
use term::{MouseButton, MouseEventKind};
#[cfg(target_os = "macos")]
use winit::os::macos::WindowExt;
struct Host {
event_loop: Rc<GuiEventLoop>,
display: glium::Display,
window_position: Option<LogicalPosition>,
/// if is_some, holds position to be restored after exiting
/// fullscreen mode.
is_fullscreen: Option<LogicalPosition>,
}
impl HostHelper for Host {
fn with_window<F: Send + 'static + Fn(&mut dyn TerminalWindow) -> Result<(), Error>>(
&self,
func: F,
) {
self.event_loop
.with_window(self.display.gl_window().id(), func);
}
fn toggle_full_screen(&mut self) {
if let Some(pos) = self.is_fullscreen.take() {
let window = self.display.gl_window();
// Use simple fullscreen mode on macos, as wez personally
// prefers the faster transition to/from this mode than
// the Lion+ slow transition to a new Space. This could
// be made into a config option if someone really wanted
// that behavior.
#[cfg(target_os = "macos")]
window.set_simple_fullscreen(false);
#[cfg(not(target_os = "macos"))]
window.set_fullscreen(None);
window.set_position(pos);
} else {
// We use our own idea of the position because get_position()
// appears to only return the initial position of the window
// on Linux.
self.is_fullscreen = self.window_position.take();
let window = self.display.gl_window();
#[cfg(target_os = "macos")]
window.set_simple_fullscreen(true);
#[cfg(not(target_os = "macos"))]
window.set_fullscreen(Some(window.get_current_monitor()));
}
}
}
pub struct GliumTerminalWindow {
host: HostImpl<Host>,
config: Arc<Config>,
fonts: Rc<FontConfiguration>,
renderer: Renderer,
width: u16,
height: u16,
cell_height: usize,
cell_width: usize,
last_mouse_coords: PhysicalPosition,
last_modifiers: KeyModifiers,
allow_received_character: bool,
mux_window_id: WindowId,
have_pending_resize_check: bool,
focus_instant: Instant,
}
impl TerminalWindow for GliumTerminalWindow {
fn get_mux_window_id(&self) -> WindowId {
self.mux_window_id
}
fn config(&self) -> &Arc<Config> {
&self.config
}
fn fonts(&self) -> &Rc<FontConfiguration> {
&self.fonts
}
fn set_window_title(&mut self, title: &str) -> Result<(), Error> {
self.host.display.gl_window().set_title(title);
Ok(())
}
fn hide_window(&mut self) {
self.host.display.gl_window().hide();
}
fn show_window(&mut self) {
self.host.display.gl_window().show();
}
fn frame(&self) -> glium::Frame {
self.host.display.draw()
}
fn renderer(&mut self) -> &mut Renderer {
&mut self.renderer
}
fn recreate_texture_atlas(&mut self, size: u32) -> Result<(), Error> {
self.renderer.recreate_atlas(&self.host.display, size)
}
fn get_dimensions(&self) -> Dimensions {
Dimensions {
width: self.width,
height: self.height,
cell_height: self.cell_height,
cell_width: self.cell_width,
}
}
fn advise_renderer_that_scaling_has_changed(
&mut self,
cell_width: usize,
cell_height: usize,
) -> Result<(), Error> {
self.cell_width = cell_width;
self.cell_height = cell_height;
// Ensure that we can't resize smaller than 1x1 with the new
// scaling factor
let size = PhysicalSize::new(cell_width as f64, cell_height as f64);
let window = self.host.display.gl_window();
let dpi = window.get_hidpi_factor();
window.set_min_dimensions(Some(size.to_logical(dpi)));
self.renderer.scaling_changed(&self.host.display)
}
fn advise_renderer_of_resize(&mut self, width: u16, height: u16) -> Result<(), Error> {
self.width = width;
self.height = height;
self.renderer.resize(&self.host.display, width, height)
}
fn resize_if_not_full_screen(&mut self, width: u16, height: u16) -> Result<bool, Error> {
if self.host.is_fullscreen.is_none() {
{
let size = PhysicalSize::new(width.into(), height.into());
let window = self.host.display.gl_window();
let dpi = window.get_hidpi_factor();
window.set_inner_size(size.to_logical(dpi));
}
self.resize_surfaces(width, height, true)?;
Ok(true)
} else {
Ok(false)
}
}
fn check_for_resize(&mut self) -> Result<(), Error> {
self.have_pending_resize_check = false;
let old_dpi_scale = self.fonts.get_dpi_scale();
let size = self
.host
.display
.gl_window()
.get_inner_size()
.ok_or_else(|| format_err!("failed to get inner window size"))?;
let dpi_scale = self.host.display.gl_window().get_hidpi_factor();
let (width, height): (u32, u32) = size.to_physical(dpi_scale).into();
if width == 0 || height == 0 {
// on windows, this can happen when minimizing the window.
// NOP!
return Ok(());
}
debug!(
"resize {}x{}@{} -> {}x{}@{}",
self.width, self.height, old_dpi_scale, width, height, dpi_scale
);
if (old_dpi_scale - dpi_scale).abs() >= std::f64::EPSILON {
self.scaling_changed(None, Some(dpi_scale), width as u16, height as u16)?;
} else {
self.resize_surfaces(width as u16, height as u16, false)?;
}
Ok(())
}
}
impl GliumTerminalWindow {
pub fn new(
event_loop: &Rc<GuiEventLoop>,
fonts: &Rc<FontConfiguration>,
config: &Arc<Config>,
tab: &Rc<dyn Tab>,
mux_window_id: WindowId,
) -> Result<GliumTerminalWindow, Error> {
let (physical_rows, physical_cols) = tab.renderer().physical_dimensions();
let metrics = fonts.default_font_metrics()?;
let (cell_height, cell_width) = (
metrics.cell_height.ceil() as usize,
metrics.cell_width.ceil() as usize,
);
let width = cell_width * physical_cols;
let height = cell_height * physical_rows;
let logical_size = LogicalSize::new(width as f64, height as f64);
debug!("make window with {}x{}", width, height);
let display = {
let pref_context = glutin::ContextBuilder::new()
.with_vsync(true)
.with_pixel_format(24, 8);
let window = glutin::WindowBuilder::new()
.with_min_dimensions(LogicalSize::new(cell_width as f64, cell_height as f64))
.with_dimensions(logical_size)
.with_window_icon(Some(glutin::Icon::from_bytes(include_bytes!(
"../../../assets/icon/terminal.png"
))?))
.with_title("wezterm");
let mut_loop = event_loop.event_loop.borrow_mut();
glium::Display::new(window, pref_context, &*mut_loop)
.map_err(|e| format_err!("{:?}", e))?
};
let window_position = display.gl_window().get_position();
let host = HostImpl::new(Host {
event_loop: Rc::clone(event_loop),
display,
window_position,
is_fullscreen: None,
});
host.display.gl_window().set_cursor(MouseCursor::Text);
#[cfg(windows)]
enable_dark_mode(&host.display.gl_window());
let width = width as u16;
let height = height as u16;
let renderer = Renderer::new(&host.display, width, height, fonts)?;
Ok(GliumTerminalWindow {
host,
config: Arc::clone(config),
fonts: Rc::clone(fonts),
renderer,
width,
height,
cell_height,
cell_width,
last_mouse_coords: PhysicalPosition::new(0.0, 0.0),
last_modifiers: Default::default(),
allow_received_character: false,
mux_window_id,
have_pending_resize_check: false,
focus_instant: Instant::now(),
})
}
pub fn window_id(&self) -> glutin::WindowId {
self.host.display.gl_window().id()
}
fn decode_modifiers(state: glium::glutin::ModifiersState) -> term::KeyModifiers {
let mut mods = Default::default();
if state.shift {
mods |= term::KeyModifiers::SHIFT;
}
if state.ctrl {
mods |= term::KeyModifiers::CTRL;
}
if state.alt {
mods |= term::KeyModifiers::ALT;
}
if state.logo {
mods |= term::KeyModifiers::SUPER;
}
mods
}
fn mouse_move(
&mut self,
position: PhysicalPosition,
modifiers: glium::glutin::ModifiersState,
) -> Result<(), Error> {
// On Windows, I've observed that we receive a continuous stream of
// CursorMoved events with the same coordinates. Let's avoid
// doing any real work in that situation.
if position == self.last_mouse_coords {
return Ok(());
}
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(self.get_mux_window_id()) {
Some(tab) => tab,
None => return Ok(()),
};
self.last_mouse_coords = position;
let (x, y): (i32, i32) = position.into();
tab.mouse_event(
term::MouseEvent {
kind: MouseEventKind::Move,
button: MouseButton::None,
x: (x as usize / self.cell_width) as usize,
y: (y as usize / self.cell_height) as i64,
modifiers: Self::decode_modifiers(modifiers),
},
&mut TabHost::new(&mut *tab.writer(), &mut self.host),
)?;
// Deliberately not forcing a paint on mouse move as it
// makes selection feel sluggish
// self.paint_if_needed()?;
// When hovering over a hyperlink, show an appropriate
// mouse cursor to give the cue that it is clickable
let cursor = if tab.renderer().current_highlight().is_some() {
MouseCursor::Hand
} else {
MouseCursor::Text
};
self.host.display.gl_window().set_cursor(cursor);
Ok(())
}
fn mouse_click(
&mut self,
state: ElementState,
button: glutin::MouseButton,
modifiers: glium::glutin::ModifiersState,
) -> Result<(), Error> {
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(self.get_mux_window_id()) {
Some(tab) => tab,
None => return Ok(()),
};
tab.mouse_event(
term::MouseEvent {
kind: match state {
ElementState::Pressed => MouseEventKind::Press,
ElementState::Released => MouseEventKind::Release,
},
button: match button {
glutin::MouseButton::Left => MouseButton::Left,
glutin::MouseButton::Right => MouseButton::Right,
glutin::MouseButton::Middle => MouseButton::Middle,
glutin::MouseButton::Other(_) => return Ok(()),
},
x: (self.last_mouse_coords.x as usize / self.cell_width) as usize,
y: (self.last_mouse_coords.y as usize / self.cell_height) as i64,
modifiers: Self::decode_modifiers(modifiers),
},
&mut TabHost::new(&mut *tab.writer(), &mut self.host),
)?;
self.paint_if_needed()?;
Ok(())
}
/// Handle a scroll wheel or touchpad scroll gesture.
/// The delta can provide either a LineDelta or a PixelData
/// depending on the source of the input.
/// On Linux with a touch pad I'm seeing fractional LineDelta
/// values depending on the velocity of my scroll swipe.
/// We need to translate this to a series of wheel events to
/// pass to the underlying terminal model.
fn mouse_wheel(
&mut self,
delta: glutin::MouseScrollDelta,
modifiers: glium::glutin::ModifiersState,
) -> Result<(), Error> {
// Figure out which wheel button and how many times we want
// to trigger it based on the magnitude of the wheel event.
// We currently only care about vertical scrolling so the code
// below will return early if all we have is horizontal scroll
// components.
let button = match delta {
glutin::MouseScrollDelta::LineDelta(_, lines) if lines > 0.0 => {
MouseButton::WheelUp(lines.abs().ceil() as usize)
}
glutin::MouseScrollDelta::LineDelta(_, lines) if lines < 0.0 => {
MouseButton::WheelDown(lines.abs().ceil() as usize)
}
glutin::MouseScrollDelta::PixelDelta(position) => {
let lines = position.y / self.cell_height as f64;
if lines > 0.0 {
MouseButton::WheelUp(lines.abs().ceil() as usize)
} else if lines < 0.0 {
MouseButton::WheelDown(lines.abs().ceil() as usize)
} else {
return Ok(());
}
}
_ => return Ok(()),
};
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(self.get_mux_window_id()) {
Some(tab) => tab,
None => return Ok(()),
};
tab.mouse_event(
term::MouseEvent {
kind: MouseEventKind::Press,
button,
x: (self.last_mouse_coords.x as usize / self.cell_width) as usize,
y: (self.last_mouse_coords.y as usize / self.cell_height) as i64,
modifiers: Self::decode_modifiers(modifiers),
},
&mut TabHost::new(&mut *tab.writer(), &mut self.host),
)?;
self.paint_if_needed()?;
Ok(())
}
/// Winit, which is the underlying windowing library, doesn't have a very consistent
/// story around how it constructs KeyboardInput instances. For example when running
/// against X11 inside WSL, the VirtualKeyCode is set to Grave when backtick is pressed,
/// but is None when `~` is pressed (shift+Grave).
/// In this situation we don't know whether ReceivedCharacter will follow with the
/// `~` translated.
/// Because we cannot trust the input data, this function is present to compute
/// a VirtualKeyCode from the scan_code.
/// This isn't great because correctly interpreting the scan_code requires more
/// system dependent context than we have available.
/// For now we have to put up with it; this may result in us effectively being
/// hardcoded to a US English keyboard layout until we come up with something
/// better.
fn scancode_to_virtual(scan_code: u32) -> Option<glium::glutin::VirtualKeyCode> {
use glium::glutin::VirtualKeyCode as V;
let code = match scan_code {
0x29 => V::Grave,
0x02 => V::Key1,
0x03 => V::Key2,
0x04 => V::Key3,
0x05 => V::Key4,
0x06 => V::Key5,
0x07 => V::Key6,
0x08 => V::Key7,
0x09 => V::Key8,
0x0a => V::Key9,
0x0b => V::Key0,
0x0c => V::Minus,
0x0d => V::Equals,
0x0e => V::Back,
0x0f => V::Tab,
0x10 => V::Q,
0x11 => V::W,
0x12 => V::E,
0x13 => V::R,
0x14 => V::T,
0x15 => V::Y,
0x16 => V::U,
0x17 => V::I,
0x18 => V::O,
0x19 => V::P,
0x1a => V::LBracket,
0x1b => V::RBracket,
0x2b => V::Backslash,
0x1e => V::A,
0x1f => V::S,
0x20 => V::D,
0x21 => V::F,
0x22 => V::G,
0x23 => V::H,
0x24 => V::J,
0x25 => V::K,
0x26 => V::L,
0x27 => V::Semicolon,
0x28 => V::Apostrophe,
0x1c => V::Return,
0x2a => V::LShift,
0x2c => V::Z,
0x2d => V::X,
0x2e => V::C,
0x2f => V::V,
0x30 => V::B,
0x31 => V::N,
0x32 => V::M,
0x33 => V::Comma,
0x34 => V::Period,
0x35 => V::Slash,
0x36 => V::RShift,
0x1d => V::LControl,
0x38 => V::RControl,
0x39 => V::Space,
0x01 => V::Escape,
_ => return None,
};
Some(code)
}
#[cfg_attr(
feature = "cargo-clippy",
allow(clippy::cyclomatic_complexity, clippy::cognitive_complexity)
)]
fn normalize_keycode(code: glium::glutin::VirtualKeyCode, shifted: bool) -> Option<KeyCode> {
use glium::glutin::VirtualKeyCode as V;
macro_rules! shifted {
($lower:expr, $upper:expr) => {
if shifted {
KeyCode::Char($upper)
} else {
KeyCode::Char($lower)
}
};
($lower:expr) => {
if shifted {
KeyCode::Char($lower.to_ascii_uppercase())
} else {
KeyCode::Char($lower)
}
};
}
let key = match code {
V::Key1 => shifted!('1', '!'),
V::Key2 => shifted!('2', '@'),
V::Key3 => shifted!('3', '#'),
V::Key4 => shifted!('4', '$'),
V::Key5 => shifted!('5', '%'),
V::Key6 => shifted!('6', '^'),
V::Key7 => shifted!('7', '&'),
V::Key8 => shifted!('8', '*'),
V::Key9 => shifted!('9', '('),
V::Key0 => shifted!('0', ')'),
V::A => shifted!('a'),
V::B => shifted!('b'),
V::C => shifted!('c'),
V::D => shifted!('d'),
V::E => shifted!('e'),
V::F => shifted!('f'),
V::G => shifted!('g'),
V::H => shifted!('h'),
V::I => shifted!('i'),
V::J => shifted!('j'),
V::K => shifted!('k'),
V::L => shifted!('l'),
V::M => shifted!('m'),
V::N => shifted!('n'),
V::O => shifted!('o'),
V::P => shifted!('p'),
V::Q => shifted!('q'),
V::R => shifted!('r'),
V::S => shifted!('s'),
V::T => shifted!('t'),
V::U => shifted!('u'),
V::V => shifted!('v'),
V::W => shifted!('w'),
V::X => shifted!('x'),
V::Y => shifted!('y'),
V::Z => shifted!('z'),
V::Return | V::NumpadEnter => KeyCode::Enter,
V::Back => KeyCode::Backspace,
V::Escape => KeyCode::Escape,
V::Delete => KeyCode::Delete,
V::Colon => KeyCode::Char(':'),
V::Space => KeyCode::Char(' '),
V::Equals => shifted!('=', '+'),
V::Add => KeyCode::Char('+'),
V::Apostrophe => shifted!('\'', '"'),
V::Backslash => shifted!('\\', '|'),
V::Grave => shifted!('`', '~'),
V::LBracket => shifted!('[', '{'),
V::Minus => shifted!('-', '_'),
V::Period => shifted!('.', '>'),
V::RBracket => shifted!(']', '}'),
V::Semicolon => shifted!(';', ':'),
V::Slash => shifted!('/', '?'),
V::Comma => shifted!(',', '<'),
V::Subtract => shifted!('-', '_'),
V::At => KeyCode::Char('@'),
V::Tab => KeyCode::Char('\t'),
V::F1 => KeyCode::Function(1),
V::F2 => KeyCode::Function(2),
V::F3 => KeyCode::Function(3),
V::F4 => KeyCode::Function(4),
V::F5 => KeyCode::Function(5),
V::F6 => KeyCode::Function(6),
V::F7 => KeyCode::Function(7),
V::F8 => KeyCode::Function(8),
V::F9 => KeyCode::Function(9),
V::F10 => KeyCode::Function(10),
V::F11 => KeyCode::Function(11),
V::F12 => KeyCode::Function(12),
V::F13 => KeyCode::Function(13),
V::F14 => KeyCode::Function(14),
V::F15 => KeyCode::Function(15),
V::Insert => KeyCode::Insert,
V::Home => KeyCode::Home,
V::End => KeyCode::End,
V::PageDown => KeyCode::PageDown,
V::PageUp => KeyCode::PageUp,
V::Left => KeyCode::LeftArrow,
V::Up => KeyCode::UpArrow,
V::Right => KeyCode::RightArrow,
V::Down => KeyCode::DownArrow,
V::LAlt | V::RAlt => KeyCode::Alt,
V::LControl | V::RControl => KeyCode::Control,
V::LShift | V::RShift => KeyCode::Shift,
V::LWin | V::RWin => KeyCode::Super,
_ => return None,
};
Some(key)
}
fn keycode_from_input(event: &glium::glutin::KeyboardInput) -> Option<KeyCode> {
if let Some(code) = event.virtual_keycode {
Self::normalize_keycode(code, event.modifiers.shift)
} else if let Some(code) = Self::scancode_to_virtual(event.scancode) {
Self::normalize_keycode(code, event.modifiers.shift)
} else {
None
}
}
fn key_event(&mut self, event: glium::glutin::KeyboardInput) -> Result<(), Error> {
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(self.get_mux_window_id()) {
Some(tab) => tab,
None => return Ok(()),
};
let mods = Self::decode_modifiers(event.modifiers);
self.last_modifiers = mods;
self.allow_received_character = false;
if let Some(key) = Self::keycode_from_input(&event) {
// debug!("event {:?} -> {:?}", event, key);
match event.state {
ElementState::Pressed => {
if self.host.process_gui_shortcuts(&*tab, mods, key)? {
return Ok(());
}
tab.key_down(key, mods)?;
}
ElementState::Released => {}
}
} else {
error!("event {:?} with no mapping", event);
}
self.paint_if_needed()?;
Ok(())
}
pub fn dispatch_event(&mut self, event: &glutin::Event) -> Result<(), Error> {
use glium::glutin::{Event, WindowEvent};
match *event {
Event::WindowEvent {
event: WindowEvent::Destroyed,
..
} => {
return Err(SessionTerminated::WindowClosed.into());
}
Event::WindowEvent {
event: WindowEvent::HiDpiFactorChanged(_),
..
}
| Event::WindowEvent {
event: WindowEvent::Resized(_),
..
} => {
if !self.have_pending_resize_check {
self.have_pending_resize_check = true;
self.host.with_window(|win| win.check_for_resize());
}
}
Event::WindowEvent {
event: WindowEvent::Focused(focus),
..
} => {
if focus {
self.focus_instant = Instant::now();
}
}
Event::WindowEvent {
event: WindowEvent::Moved(position),
..
} => {
self.host.window_position = Some(position);
}
Event::WindowEvent {
event: WindowEvent::ReceivedCharacter(c),
..
} => {
// Coupled with logic in key_event which gates whether
// we allow processing unicode chars here
// debug!("ReceivedCharacter {} {:?}", c as u32, c);
if self.allow_received_character {
self.allow_received_character = false;
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(self.get_mux_window_id()) {
Some(tab) => tab,
None => return Ok(()),
};
tab.key_down(KeyCode::Char(c), self.last_modifiers)?;
self.paint_if_needed()?;
}
return Ok(());
}
Event::WindowEvent {
event: WindowEvent::KeyboardInput { input, .. },
..
} => {
self.key_event(input)?;
}
Event::WindowEvent {
event:
WindowEvent::CursorMoved {
position,
modifiers,
..
},
..
} => {
let dpi_scale = self.host.display.gl_window().get_hidpi_factor();
self.mouse_move(position.to_physical(dpi_scale), modifiers)?;
}
Event::WindowEvent {
event:
WindowEvent::MouseInput {
state,
button,
modifiers,
..
},
..
} => {
// When focusing the window, allow a brief grace period and ignore
// clicks within that interval. If we don't do this, the click will
// clear the current selection which is super annoying if you just
// copied some text in another app, and then clicked on the terminal
// to paste it.
if Instant::now() - self.focus_instant > Duration::from_millis(200) {
self.mouse_click(state, button, modifiers)?;
}
}
Event::WindowEvent {
event:
WindowEvent::MouseWheel {
delta, modifiers, ..
},
..
} => {
self.mouse_wheel(delta, modifiers)?;
}
Event::WindowEvent {
event: WindowEvent::Refresh,
..
} => {
self.paint()?;
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
self.host.close_current_tab();
}
_ => {}
}
Ok(())
}
}
#[cfg(windows)]
fn enable_dark_mode(window: &winit::Window) {
// Prefer to run in dark mode. This could be made configurable without
// a huge amount of effort, but I think it's fine to just be always
// dark mode by default :-p
// Note that the MS terminal app uses the logic found here for this
// stuff:
// https://github.com/microsoft/terminal/blob/9b92986b49bed8cc41fde4d6ef080921c41e6d9e/src/interactivity/win32/windowtheme.cpp#L62
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use winapi::shared::minwindef::{BOOL, DWORD};
use winapi::um::dwmapi::DwmSetWindowAttribute;
use winapi::um::uxtheme::SetWindowTheme;
use winit::os::windows::WindowExt;
fn wide_str(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
let hwnd = window.get_hwnd();
const DWMWA_USE_IMMERSIVE_DARK_MODE: DWORD = 19;
unsafe {
SetWindowTheme(
hwnd as _,
wide_str("DarkMode_Explorer").as_slice().as_ptr(),
std::ptr::null_mut(),
);
let enabled: BOOL = 1;
DwmSetWindowAttribute(
hwnd as _,
DWMWA_USE_IMMERSIVE_DARK_MODE,
&enabled as *const _ as *const _,
std::mem::size_of_val(&enabled) as u32,
);
}
}

View File

@ -1,242 +0,0 @@
#![cfg(feature = "enable-winit")]
use super::window::TerminalWindow;
use crate::font::{FontConfiguration, FontSystemSelection};
use crate::frontend::guicommon::clipboard::SystemClipboard;
use crate::frontend::{front_end, gui_executor};
use crate::keyassignment::{KeyAssignment, KeyMap};
use crate::mux::tab::Tab;
use crate::mux::Mux;
use failure::Error;
use failure::Fallible;
use log::error;
use portable_pty::PtySize;
use promise::Future;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use std::sync::Arc;
use term::terminal::Clipboard;
use term::{KeyCode, KeyModifiers};
use termwiz::hyperlink::Hyperlink;
pub trait HostHelper {
fn with_window<F: Send + 'static + Fn(&mut dyn TerminalWindow) -> Result<(), Error>>(
&self,
func: F,
);
fn toggle_full_screen(&mut self);
}
pub struct HostImpl<H: HostHelper> {
helper: H,
clipboard: Arc<dyn Clipboard>,
keys: KeyMap,
}
impl<H: HostHelper> HostImpl<H> {
pub fn new(helper: H) -> Self {
Self {
helper,
clipboard: Arc::new(SystemClipboard::new()),
keys: KeyMap::new(),
}
}
pub fn get_clipboard(&mut self) -> Fallible<Arc<dyn Clipboard>> {
Ok(Arc::clone(&self.clipboard))
}
pub fn spawn_new_window(&mut self) {
Future::with_executor(gui_executor().unwrap(), move || {
let mux = Mux::get().unwrap();
let fonts = Rc::new(FontConfiguration::new(
Arc::clone(mux.config()),
FontSystemSelection::get_default(),
));
let window_id = mux.new_empty_window();
let tab = mux
.default_domain()
.spawn(PtySize::default(), None, window_id)?;
let front_end = front_end().expect("to be called on gui thread");
front_end.spawn_new_window(mux.config(), &fonts, &tab, window_id)?;
Ok(())
});
}
pub fn perform_key_assignment(
&mut self,
tab: &dyn Tab,
assignment: &KeyAssignment,
) -> Fallible<()> {
use KeyAssignment::*;
match assignment {
SpawnTab(spawn_where) => {
let spawn_where = spawn_where.clone();
self.with_window(move |win| win.spawn_tab(&spawn_where).map(|_| ()))
}
SpawnWindow => self.spawn_new_window(),
ToggleFullScreen => self.toggle_full_screen(),
Copy => {
// Nominally copy, but that is implicit, so NOP
}
Paste => {
tab.trickle_paste(self.get_clipboard()?.get_contents()?)?;
}
ActivateTabRelative(n) => self.activate_tab_relative(*n),
DecreaseFontSize => self.decrease_font_size(),
IncreaseFontSize => self.increase_font_size(),
ResetFontSize => self.reset_font_size(),
ActivateTab(n) => self.activate_tab(*n),
SendString(s) => tab.writer().write_all(s.as_bytes())?,
Hide => self.hide_window(),
Show => self.show_window(),
CloseCurrentTab => self.close_current_tab(),
Nop => {}
}
Ok(())
}
pub fn process_gui_shortcuts(
&mut self,
tab: &dyn Tab,
mods: KeyModifiers,
key: KeyCode,
) -> Result<bool, Error> {
if let Some(assignment) = self.keys.lookup(key, mods) {
self.perform_key_assignment(tab, &assignment)?;
Ok(true)
} else {
Ok(false)
}
}
pub fn activate_tab(&mut self, tab: usize) {
self.with_window(move |win| win.activate_tab(tab))
}
pub fn activate_tab_relative(&mut self, tab: isize) {
self.with_window(move |win| win.activate_tab_relative(tab))
}
pub fn increase_font_size(&mut self) {
self.with_window(move |win| {
let scale = win.fonts().get_font_scale();
let dims = win.get_dimensions();
win.scaling_changed(Some(scale * 1.1), None, dims.width, dims.height)
})
}
pub fn decrease_font_size(&mut self) {
self.with_window(move |win| {
let scale = win.fonts().get_font_scale();
let dims = win.get_dimensions();
win.scaling_changed(Some(scale * 0.9), None, dims.width, dims.height)
})
}
pub fn reset_font_size(&mut self) {
self.with_window(move |win| {
let dims = win.get_dimensions();
win.scaling_changed(Some(1.0), None, dims.width, dims.height)
})
}
pub fn close_current_tab(&mut self) {
self.with_window(move |win| {
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(win.get_mux_window_id()) {
Some(tab) => tab,
None => return Ok(()),
};
mux.remove_tab(tab.tab_id());
if let Some(mut win) = mux.get_window_mut(win.get_mux_window_id()) {
win.remove_by_id(tab.tab_id());
}
win.activate_tab_relative(0)
});
}
pub fn hide_window(&mut self) {
self.with_window(move |win| {
win.hide_window();
Ok(())
});
}
pub fn show_window(&mut self) {
self.with_window(move |win| {
win.show_window();
Ok(())
});
}
}
impl<H: HostHelper> Deref for HostImpl<H> {
type Target = H;
fn deref(&self) -> &H {
&self.helper
}
}
impl<H: HostHelper> DerefMut for HostImpl<H> {
fn deref_mut(&mut self) -> &mut H {
&mut self.helper
}
}
/// Implements `TerminalHost` for a Tab.
/// `TabHost` instances are short lived and borrow references to
/// other state.
pub struct TabHost<'a, H: HostHelper> {
writer: &'a mut dyn std::io::Write,
host: &'a mut HostImpl<H>,
}
impl<'a, H: HostHelper> TabHost<'a, H> {
pub fn new(writer: &'a mut dyn std::io::Write, host: &'a mut HostImpl<H>) -> Self {
Self { writer, host }
}
}
impl<'a, H: HostHelper> term::TerminalHost for TabHost<'a, H> {
fn writer(&mut self) -> &mut dyn std::io::Write {
&mut self.writer
}
fn click_link(&mut self, link: &Arc<Hyperlink>) {
match open::that(link.uri()) {
Ok(_) => {}
Err(err) => error!("failed to open {}: {:?}", link.uri(), err),
}
}
fn get_clipboard(&mut self) -> Fallible<Arc<dyn Clipboard>> {
self.host.get_clipboard()
}
fn set_title(&mut self, _title: &str) {
self.host.with_window(move |win| {
win.update_title();
Ok(())
})
}
fn activate_tab(&mut self, tab: usize) {
self.host.activate_tab(tab)
}
fn activate_tab_relative(&mut self, tab: isize) {
self.host.activate_tab_relative(tab)
}
fn increase_font_size(&mut self) {
self.host.increase_font_size()
}
fn decrease_font_size(&mut self) {
self.host.decrease_font_size()
}
fn reset_font_size(&mut self) {
self.host.reset_font_size()
}
}

View File

@ -1,4 +1,2 @@
pub mod clipboard;
pub mod host;
pub mod localtab;
pub mod window;

View File

@ -1,335 +0,0 @@
#![cfg(feature = "enable-winit")]
use crate::config::Config;
use crate::font::FontConfiguration;
use crate::keyassignment::SpawnTabDomain;
use crate::mux::tab::{Tab, TabId};
use crate::mux::window::WindowId;
use crate::mux::Mux;
use crate::opengl::render::Renderer;
use crate::opengl::textureatlas::OutOfTextureSpace;
use glium;
use portable_pty::PtySize;
use std::rc::Rc;
use std::sync::Arc;
/// Reports the currently configured physical size of the display
/// surface (physical pixels, not adjusted for dpi) and the current
/// cell dimensions, also in physical pixels
pub struct Dimensions {
pub width: u16,
pub height: u16,
pub cell_height: usize,
pub cell_width: usize,
}
/// This trait is used to share implementations of common code between
/// the different GUI systems.
/// A number of methods need to be provided by the window in order to
/// unlock the use of the provided methods towards the bottom of the trait.
pub trait TerminalWindow {
fn set_window_title(&mut self, title: &str) -> failure::Fallible<()>;
fn get_mux_window_id(&self) -> WindowId;
fn frame(&self) -> glium::Frame;
fn renderer(&mut self) -> &mut Renderer;
fn recreate_texture_atlas(&mut self, size: u32) -> failure::Fallible<()>;
fn advise_renderer_that_scaling_has_changed(
&mut self,
cell_width: usize,
cell_height: usize,
) -> failure::Fallible<()>;
fn advise_renderer_of_resize(&mut self, width: u16, height: u16) -> failure::Fallible<()>;
fn config(&self) -> &Arc<Config>;
fn fonts(&self) -> &Rc<FontConfiguration>;
fn get_dimensions(&self) -> Dimensions;
fn resize_if_not_full_screen(&mut self, width: u16, height: u16) -> failure::Fallible<bool>;
fn check_for_resize(&mut self) -> failure::Fallible<()> {
Ok(())
}
fn hide_window(&mut self) {}
fn show_window(&mut self) {}
fn activate_tab(&mut self, tab_idx: usize) -> failure::Fallible<()> {
let mux = Mux::get().unwrap();
let mut window = mux
.get_window_mut(self.get_mux_window_id())
.ok_or_else(|| failure::format_err!("no such window"))?;
let max = window.len();
if tab_idx < max {
window.set_active(tab_idx);
drop(window);
self.update_title();
}
Ok(())
}
fn activate_tab_relative(&mut self, delta: isize) -> failure::Fallible<()> {
let mux = Mux::get().unwrap();
let window = mux
.get_window(self.get_mux_window_id())
.ok_or_else(|| failure::format_err!("no such window"))?;
let max = window.len();
failure::ensure!(max > 0, "no more tabs");
let active = window.get_active_idx() as isize;
let tab = active + delta;
let tab = if tab < 0 { max as isize + tab } else { tab };
drop(window);
self.activate_tab(tab as usize % max)
}
fn update_title(&mut self) {
let mux = Mux::get().unwrap();
let window = match mux.get_window(self.get_mux_window_id()) {
Some(window) => window,
_ => return,
};
let num_tabs = window.len();
if num_tabs == 0 {
return;
}
let tab_no = window.get_active_idx();
let title = match window.get_active() {
Some(tab) => tab.get_title(),
None => return,
};
drop(window);
if num_tabs == 1 {
self.set_window_title(&title).ok();
} else {
self.set_window_title(&format!("[{}/{}] {}", tab_no + 1, num_tabs, title))
.ok();
}
}
fn paint_if_needed(&mut self) -> failure::Fallible<()> {
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(self.get_mux_window_id()) {
Some(tab) => tab,
None => return Ok(()),
};
if tab.renderer().has_dirty_lines() {
self.paint()?;
}
self.update_title();
Ok(())
}
fn paint(&mut self) -> failure::Fallible<()> {
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(self.get_mux_window_id()) {
Some(tab) => tab,
None => return Ok(()),
};
let start = std::time::Instant::now();
let mut target = self.frame();
let res = {
let renderer = self.renderer();
let palette = tab.palette();
renderer.paint(&mut target, &mut *tab.renderer(), &palette)
};
// Ensure that we finish() the target before we let the
// error bubble up, otherwise we lose the context.
target
.finish()
.expect("target.finish failed and we don't know how to recover");
log::debug!("paint elapsed={:?}", start.elapsed());
// The only error we want to catch is texture space related;
// when that happens we need to blow our glyph cache and
// allocate a newer bigger texture.
match res {
Err(err) => {
if let Some(&OutOfTextureSpace { size }) = err.downcast_ref::<OutOfTextureSpace>() {
log::error!("out of texture space, allocating {}", size);
self.recreate_texture_atlas(size)?;
tab.renderer().make_all_lines_dirty();
// Recursively initiate a new paint
return self.paint();
}
Err(err)
}
Ok(_) => Ok(()),
}
}
fn spawn_tab(&mut self, domain: &SpawnTabDomain) -> failure::Fallible<TabId> {
let dims = self.get_dimensions();
let rows = (dims.height as usize + 1) / dims.cell_height;
let cols = (dims.width as usize + 1) / dims.cell_width;
let size = PtySize {
rows: rows as u16,
cols: cols as u16,
pixel_width: dims.width,
pixel_height: dims.height,
};
let mux = Mux::get().unwrap();
let domain = match domain {
SpawnTabDomain::DefaultDomain => mux.default_domain().clone(),
SpawnTabDomain::CurrentTabDomain => {
let tab = match mux.get_active_tab_for_window(self.get_mux_window_id()) {
Some(tab) => tab,
None => failure::bail!("window has no tabs?"),
};
mux.get_domain(tab.domain_id()).ok_or_else(|| {
failure::format_err!("current tab has unresolvable domain id!?")
})?
}
SpawnTabDomain::Domain(id) => mux.get_domain(*id).ok_or_else(|| {
failure::format_err!("spawn_tab called with unresolvable domain id!?")
})?,
SpawnTabDomain::DomainName(name) => mux.get_domain_by_name(&name).ok_or_else(|| {
failure::format_err!("spawn_tab called with unresolvable domain name {}", name)
})?,
};
let tab = domain.spawn(size, None, self.get_mux_window_id())?;
let tab_id = tab.tab_id();
let len = {
let window = mux
.get_window(self.get_mux_window_id())
.ok_or_else(|| failure::format_err!("no such window!?"))?;
window.len()
};
self.activate_tab(len - 1)?;
Ok(tab_id)
}
fn resize_surfaces(&mut self, width: u16, height: u16, force: bool) -> failure::Fallible<bool> {
let dims = self.get_dimensions();
if force || width != dims.width || height != dims.height {
log::debug!("resize {},{}", width, height);
self.advise_renderer_of_resize(width, height)?;
// The +1 in here is to handle an irritating case.
// When we get N rows with a gap of cell_height - 1 left at
// the bottom, we can usually squeeze that extra row in there,
// so optimistically pretend that we have that extra pixel!
let rows = ((height as usize + 1) / dims.cell_height) as u16;
let cols = ((width as usize + 1) / dims.cell_width) as u16;
let mux = Mux::get().unwrap();
let window = mux
.get_window(self.get_mux_window_id())
.ok_or_else(|| failure::format_err!("no such window!?"))?;
for tab in window.iter() {
tab.resize(PtySize {
rows,
cols,
pixel_width: width as u16,
pixel_height: height as u16,
})?;
}
Ok(true)
} else {
log::debug!("ignoring extra resize");
Ok(false)
}
}
fn scaling_changed(
&mut self,
font_scale: Option<f64>,
dpi_scale: Option<f64>,
width: u16,
height: u16,
) -> failure::Fallible<()> {
let fonts = self.fonts();
let dpi_scale = dpi_scale.unwrap_or_else(|| fonts.get_dpi_scale());
let font_scale = font_scale.unwrap_or_else(|| fonts.get_font_scale());
log::debug!(
"TerminalWindow::scaling_changed dpi_scale={} font_scale={}",
dpi_scale,
font_scale
);
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(self.get_mux_window_id()) {
Some(tab) => tab,
None => return Ok(()),
};
tab.renderer().make_all_lines_dirty();
fonts.change_scaling(font_scale, dpi_scale);
let metrics = fonts.default_font_metrics()?;
let (cell_height, cell_width) = (metrics.cell_height, metrics.cell_width);
// It is desirable to preserve the terminal rows/cols when scaling,
// so we query for that information here.
// If the backend supports `resize_if_not_full_screen` then we'll try
// to resize the window to match the new cell metrics.
let (rows, cols) = { tab.renderer().physical_dimensions() };
self.advise_renderer_that_scaling_has_changed(
cell_width.ceil() as usize,
cell_height.ceil() as usize,
)?;
if !self.resize_if_not_full_screen(
cell_width.ceil() as u16 * cols as u16,
cell_height.ceil() as u16 * rows as u16,
)? {
self.resize_surfaces(width, height, true)?;
}
Ok(())
}
fn tab_did_terminate(&mut self, tab_id: TabId) {
let mux = Mux::get().unwrap();
let mut window = match mux.get_window_mut(self.get_mux_window_id()) {
Some(window) => window,
None => return,
};
window.remove_by_id(tab_id);
if let Some(active) = window.get_active() {
active.renderer().make_all_lines_dirty();
}
drop(window);
self.update_title();
}
// let_and_return is needed here to satisfy the borrow checker
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
fn test_for_child_exit(&mut self) -> bool {
let mux = Mux::get().unwrap();
let window = match mux.get_window(self.get_mux_window_id()) {
Some(window) => window,
None => return true,
};
let dead_tabs: Vec<Rc<dyn Tab>> = window
.iter()
.filter_map(|tab| {
if tab.is_dead() {
Some(Rc::clone(tab))
} else {
None
}
})
.collect();
drop(window);
for tab in dead_tabs {
self.tab_did_terminate(tab.tab_id());
}
let empty = match mux.get_window(self.get_mux_window_id()) {
Some(window) => window.is_empty(),
None => true,
};
empty
}
}

View File

@ -12,30 +12,21 @@ use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
#[cfg(feature = "enable-winit")]
pub mod glium;
pub mod guicommon;
pub mod muxserver;
pub mod software;
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum FrontEndSelection {
Glutin,
OpenGL,
Software,
MuxServer,
Null,
Software,
OpenGL,
}
impl Default for FrontEndSelection {
fn default() -> Self {
if cfg!(all(unix, not(target_os = "macos"))) {
FrontEndSelection::OpenGL
} else if cfg!(feature = "enable-winit") {
FrontEndSelection::Glutin
} else {
FrontEndSelection::OpenGL
}
FrontEndSelection::OpenGL
}
}
@ -67,10 +58,6 @@ pub fn front_end() -> Option<Rc<dyn FrontEnd>> {
impl FrontEndSelection {
pub fn try_new(self, mux: &Rc<Mux>) -> Result<Rc<dyn FrontEnd>, Error> {
let front_end = match self {
#[cfg(feature = "enable-winit")]
FrontEndSelection::Glutin => glium::glutinloop::GlutinFrontEnd::try_new(mux),
#[cfg(not(feature = "enable-winit"))]
FrontEndSelection::Glutin => failure::bail!("Glutin not compiled in"),
FrontEndSelection::MuxServer => muxserver::MuxServerFrontEnd::try_new(mux),
FrontEndSelection::Null => muxserver::MuxServerFrontEnd::new_null(mux),
FrontEndSelection::Software => software::SoftwareFrontEnd::try_new_no_opengl(mux),
@ -85,7 +72,7 @@ impl FrontEndSelection {
// TODO: find or build a proc macro for this
pub fn variants() -> Vec<&'static str> {
vec!["Glutin", "MuxServer", "Null", "Software", "OpenGL"]
vec!["OpenGL", "Software", "MuxServer", "Null"]
}
}
@ -93,7 +80,6 @@ impl std::str::FromStr for FrontEndSelection {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_ref() {
"glutin" => Ok(FrontEndSelection::Glutin),
"muxserver" => Ok(FrontEndSelection::MuxServer),
"null" => Ok(FrontEndSelection::Null),
"software" => Ok(FrontEndSelection::Software),

View File

@ -19,8 +19,6 @@ mod frontend;
mod keyassignment;
mod mux;
#[cfg(feature = "enable-winit")]
mod opengl;
mod ratelim;
mod server;
mod ssh;

View File

@ -1,2 +0,0 @@
pub mod render;
pub mod textureatlas;

File diff suppressed because it is too large Load Diff

View File

@ -1,244 +0,0 @@
//! Keeping track of sprite textures
use failure::Error;
use failure_derive::*;
use glium::backend::Facade;
use glium::texture::{SrgbTexture2d, Texture2dDataSource};
use glium::{self, Rect};
use std::rc::Rc;
pub const TEX_SIZE: u32 = 4096;
#[derive(Debug, Fail)]
#[fail(display = "Texture Size exceeded, need {}", size)]
pub struct OutOfTextureSpace {
pub size: u32,
}
/// Atlases are bitmaps of srgba data that are sized as a power of 2.
/// We allocate sprites out of the available space, starting from the
/// bottom left corner and working to the right until we run out of
/// space, then we move up to the logical row above. Since sprites can
/// have varying height the height of the rows can also vary.
#[derive(Debug)]
pub struct Atlas {
texture: Rc<SrgbTexture2d>,
// Dimensions of the texture
side: u32,
/// The bottom of the available space.
bottom: u32,
/// The height of the tallest sprite allocated on the current row
tallest: u32,
/// How far along the current row we've progressed
left: u32,
}
impl Atlas {
pub fn new<F: Facade>(facade: &F, side: u32) -> Result<Self, Error> {
let texture = Rc::new(SrgbTexture2d::empty_with_format(
facade,
glium::texture::SrgbFormat::U8U8U8U8,
glium::texture::MipmapsOption::NoMipmap,
side,
side,
)?);
Ok(Self {
texture,
side,
bottom: 0,
tallest: 0,
left: 0,
})
}
#[inline]
pub fn texture(&self) -> Rc<SrgbTexture2d> {
Rc::clone(&self.texture)
}
/// Reserve space for a sprite of the given size
pub fn allocate<'a, T: Texture2dDataSource<'a>>(
&mut self,
width: u32,
height: u32,
data: T,
) -> Result<Sprite, OutOfTextureSpace> {
// We pad each sprite reservation with blank space to avoid
// surprising and unexpected artifacts when the texture is
// interpolated on to the render surface.
// In addition, we need to ensure that the bottom left pixel
// is blank as we use that for whitespace glyphs.
let reserve_width = width + 2;
let reserve_height = height + 2;
if reserve_width > self.side || reserve_height > self.side {
// It's not possible to satisfy that request
return Err(OutOfTextureSpace {
size: reserve_width.max(reserve_height).next_power_of_two(),
});
}
let x_left = self.side - self.left;
if x_left < reserve_width {
// Bump up to next row
self.bottom += self.tallest;
self.left = 0;
self.tallest = 0;
}
// Do we have vertical space?
let y_left = self.side - self.bottom;
if y_left < reserve_height {
// No room at the inn.
return Err(OutOfTextureSpace {
size: (self.side + reserve_width.max(reserve_height)).next_power_of_two(),
});
}
let rect = Rect {
left: self.left + 1,
bottom: self.bottom + 1,
width,
height,
};
self.texture.write(rect, data);
self.left += reserve_width;
self.tallest = self.tallest.max(reserve_height);
Ok(Sprite {
texture: Rc::clone(&self.texture),
coords: rect,
})
}
}
#[derive(Debug)]
pub struct Sprite {
pub texture: Rc<SrgbTexture2d>,
pub coords: Rect,
}
/// Represents a vertical slice through a sprite.
/// These are used to handle multi-cell wide glyphs.
/// Each cell is nominally `cell_width` wide but font metrics
/// may result in the glyphs being wider than this.
#[derive(Debug)]
pub struct SpriteSlice {
/// This is glyph X out of num_cells
pub cell_idx: usize,
/// How many cells comprise this glyph
pub num_cells: usize,
/// The nominal width of each cell
pub cell_width: usize,
/// The glyph will be scaled from sprite pixels down to
/// cell pixels by this factor.
pub scale: f32,
/// The font metrics will adjust the left-most pixel
/// by this amount. This causes the width of cell 0
/// to be adjusted by this same amount.
pub left_offset: f32,
}
impl Sprite {
/// Returns the scaled offset to the left most pixel in a slice.
/// This is 0 for the first slice and increases by the slice_width
/// as we work through the slices.
pub fn left_pix(&self, slice: &SpriteSlice) -> f32 {
let width = self.coords.width as f32 * slice.scale;
if slice.num_cells == 1 || slice.cell_idx == 0 {
0.0
} else {
// Width of the first cell
let cell_0 = width.min((slice.cell_width as f32) - slice.left_offset);
if slice.cell_idx == slice.num_cells - 1 {
// Width of all the other cells
let middle = slice.cell_width * (slice.num_cells - 2);
cell_0 + middle as f32
} else {
// Width of all the preceding cells
let prev = slice.cell_width * slice.cell_idx;
cell_0 + prev as f32
}
}
}
/// Returns the (scaled) pixel width of a slice.
/// This is nominally the cell_width but can be modified by being the first
/// or last in a sequence of potentially oversized sprite slices.
pub fn slice_width(&self, slice: &SpriteSlice) -> f32 {
let width = self.coords.width as f32 * slice.scale;
if slice.num_cells == 1 {
width
} else if slice.cell_idx == 0 {
// The first slice can extend (or recede) to the left based
// on the slice.left_offset value.
width.min((slice.cell_width as f32) - slice.left_offset)
} else if slice.cell_idx == slice.num_cells - 1 {
width - self.left_pix(slice)
} else {
// somewhere in the middle of the sequence, the width is
// simply the cell_width
slice.cell_width as f32
}
}
/// Returns the left coordinate for a slice in texture coordinate space
#[inline]
pub fn left(&self, slice: &SpriteSlice) -> f32 {
let left = self.coords.left as f32 + (self.left_pix(slice) / slice.scale);
left / self.texture.width() as f32
}
/// Returns the right coordinate for a slice in texture coordinate space
#[inline]
pub fn right(&self, slice: &SpriteSlice) -> f32 {
let right = self.coords.left as f32
+ ((self.left_pix(slice) + self.slice_width(slice)) as f32 / slice.scale);
right / self.texture.width() as f32
}
/// Returns the top coordinate for a slice in texture coordinate space
#[inline]
pub fn top(&self, _slice: &SpriteSlice) -> f32 {
self.coords.bottom as f32 / self.texture.height() as f32
}
/// Returns the bottom coordinate for a slice in texture coordinate space
#[inline]
pub fn bottom(&self, _slice: &SpriteSlice) -> f32 {
(self.coords.bottom + self.coords.height) as f32 / self.texture.height() as f32
}
/// Returns the top-left coordinate for a slice in texture coordinate space
#[inline]
pub fn top_left(&self, slice: &SpriteSlice) -> (f32, f32) {
(self.left(slice), self.top(slice))
}
/// Returns the bottom-left coordinate for a slice in texture coordinate
/// space
#[inline]
pub fn bottom_left(&self, slice: &SpriteSlice) -> (f32, f32) {
(self.left(slice), self.bottom(slice))
}
/// Returns the bottom-right coordinate for a slice in texture coordinate
/// space
#[inline]
pub fn bottom_right(&self, slice: &SpriteSlice) -> (f32, f32) {
(self.right(slice), self.bottom(slice))
}
/// Returns the top-right coordinate for a slice in texture coordinate space
#[inline]
pub fn top_right(&self, slice: &SpriteSlice) -> (f32, f32) {
(self.right(slice), self.top(slice))
}
}