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:
parent
66f5f6842d
commit
4cb74e68dd
@ -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]
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
pub mod glutinloop;
|
||||
pub mod window;
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,4 +1,2 @@
|
||||
pub mod clipboard;
|
||||
pub mod host;
|
||||
pub mod localtab;
|
||||
pub mod window;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -19,8 +19,6 @@ mod frontend;
|
||||
mod keyassignment;
|
||||
mod mux;
|
||||
|
||||
#[cfg(feature = "enable-winit")]
|
||||
mod opengl;
|
||||
mod ratelim;
|
||||
mod server;
|
||||
mod ssh;
|
||||
|
@ -1,2 +0,0 @@
|
||||
pub mod render;
|
||||
pub mod textureatlas;
|
1026
src/opengl/render.rs
1026
src/opengl/render.rs
File diff suppressed because it is too large
Load Diff
@ -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))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user