1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 13:21:38 +03:00

add PaneSelect key assignment

This is still a bit of a WIP, but this commit:

* Introduces a new "Modal" concept to the GUI layer. The intent is
  that modal intercepts key and mouse events while active, and renders
  over the top of the rest of the normal display.
  I think there might be a couple of cases where key events skirt
  through this, but this is good enough as a first step.
  Also, the render is forced into layer 1 which has some funny side
  effects: if the modal choses to render transparent, it will poke
  a hole in the window because all the rendering happens together:
  there aren't distinct layer compositing passes.

* Add a new PaneSelect action that is implemented as a modal.
  It uses quickselect style alphabet -> pane label generation and
  renders the labels over ~the middle of each pane using an
  enlarged version of the window frame font.  Typing the label
  will activate that pane.  Escape will cancel the modal.

More styling and docs will follow in a later commit.

refs: #1975
This commit is contained in:
Wez Furlong 2022-05-23 08:12:11 -07:00
parent efb8d14064
commit 5bf736bd21
10 changed files with 352 additions and 10 deletions

View File

@ -90,6 +90,9 @@ pub struct Config {
#[dynamic(default)]
pub window_frame: WindowFrameConfig,
#[dynamic(default = "default_pane_select_font_size")]
pub pane_select_font_size: f64,
#[dynamic(default)]
pub tab_bar_style: TabBarStyle,
@ -1131,6 +1134,10 @@ impl Config {
}
}
fn default_pane_select_font_size() -> f64 {
36.0
}
fn default_swallow_mouse_click_on_window_focus() -> bool {
cfg!(target_os = "macos")
}

View File

@ -264,6 +264,13 @@ impl Default for ClipboardPasteSource {
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, FromDynamic, ToDynamic)]
pub struct PaneSelectArguments {
/// Overrides the main quick_select_alphabet config
#[dynamic(default)]
pub alphabet: String,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, FromDynamic, ToDynamic)]
pub struct QuickSelectArguments {
/// Overrides the main quick_select_alphabet config
@ -372,6 +379,7 @@ pub enum KeyAssignment {
CopyMode(CopyModeAssignment),
RotatePanes(RotationDirection),
SplitPane(SplitPane),
PaneSelect(PaneSelectArguments),
}
impl_lua_conversion_dynamic!(KeyAssignment);

View File

@ -429,6 +429,7 @@ struct FontConfigInner {
built_in: RefCell<Arc<FontDatabase>>,
no_glyphs: RefCell<HashSet<char>>,
title_font: RefCell<Option<Rc<LoadedFont>>>,
pane_select_font: RefCell<Option<Rc<LoadedFont>>>,
fallback_channel: RefCell<Option<Sender<FallbackResolveInfo>>>,
}
@ -447,6 +448,7 @@ impl FontConfigInner {
locator,
metrics: RefCell::new(None),
title_font: RefCell::new(None),
pane_select_font: RefCell::new(None),
font_scale: RefCell::new(1.0),
dpi: RefCell::new(dpi),
config: RefCell::new(config.clone()),
@ -463,6 +465,7 @@ impl FontConfigInner {
// Config was reloaded, invalidate our caches
fonts.clear();
self.title_font.borrow_mut().take();
self.pane_select_font.borrow_mut().take();
self.metrics.borrow_mut().take();
self.no_glyphs.borrow_mut().clear();
*self.font_dirs.borrow_mut() = Arc::new(FontDatabase::with_font_dirs(config)?);
@ -544,18 +547,15 @@ impl FontConfigInner {
)
}
fn title_font(&self, myself: &Rc<Self>) -> anyhow::Result<Rc<LoadedFont>> {
fn make_title_font_impl(
&self,
myself: &Rc<Self>,
pref_size: Option<f64>,
) -> anyhow::Result<Rc<LoadedFont>> {
let config = self.config.borrow();
let mut title_font = self.title_font.borrow_mut();
if let Some(entry) = title_font.as_ref() {
return Ok(Rc::clone(entry));
}
let (sys_font, sys_size) = self.compute_title_font(&config);
let font_size = config.window_frame.font_size.unwrap_or(sys_size);
let font_size = pref_size.unwrap_or(sys_size);
let text_style = config
.window_frame
@ -591,11 +591,41 @@ impl FontConfigInner {
id: alloc_font_id(),
});
Ok(loaded)
}
fn title_font(&self, myself: &Rc<Self>) -> anyhow::Result<Rc<LoadedFont>> {
let config = self.config.borrow();
let mut title_font = self.title_font.borrow_mut();
if let Some(entry) = title_font.as_ref() {
return Ok(Rc::clone(entry));
}
let loaded = self.make_title_font_impl(myself, config.window_frame.font_size)?;
title_font.replace(Rc::clone(&loaded));
Ok(loaded)
}
fn pane_select_font(&self, myself: &Rc<Self>) -> anyhow::Result<Rc<LoadedFont>> {
let config = self.config.borrow();
let mut pane_select_font = self.pane_select_font.borrow_mut();
if let Some(entry) = pane_select_font.as_ref() {
return Ok(Rc::clone(entry));
}
let loaded = self.make_title_font_impl(myself, Some(config.pane_select_font_size))?;
pane_select_font.replace(Rc::clone(&loaded));
Ok(loaded)
}
fn resolve_font_helper_impl(
&self,
attributes: &[FontAttributes],
@ -934,6 +964,10 @@ impl FontConfiguration {
self.inner.title_font(&self.inner)
}
pub fn pane_select_font(&self) -> anyhow::Result<Rc<LoadedFont>> {
self.inner.pane_select_font(&self.inner)
}
/// Given a text style, load (with caching) the font that best
/// matches according to the fontconfig pattern.
pub fn resolve_font(&self, style: &TextStyle) -> anyhow::Result<Rc<LoadedFont>> {

View File

@ -54,7 +54,7 @@ const PATTERNS: [&str; 14] = [
/// This function computes a set of labels for a given alphabet.
/// It is derived from https://github.com/fcsonline/tmux-thumbs/blob/master/src/alphabets.rs
/// which is Copyright (c) 2019 Ferran Basora and provided under the MIT license
fn compute_labels_for_alphabet(alphabet: &str, num_matches: usize) -> Vec<String> {
pub fn compute_labels_for_alphabet(alphabet: &str, num_matches: usize) -> Vec<String> {
let alphabet = alphabet
.chars()
.map(|c| c.to_lowercase().to_string())

View File

@ -278,6 +278,13 @@ impl super::TermWindow {
);
}
if let Some(modal) = self.get_modal() {
if is_down {
return modal.key_down(term_key, tw_raw_modifiers, self).is_ok();
}
return false;
}
let res = if is_down {
pane.key_down(term_key, tw_raw_modifiers)
} else {
@ -512,6 +519,13 @@ impl super::TermWindow {
);
}
if let Some(modal) = self.get_modal() {
if window_key.key_is_down {
modal.key_down(key, modifiers, self).ok();
}
return;
}
let res = if let Some(encoded) = self.encode_win32_input(&pane, &window_key) {
if self.config.debug_key_events {
log::info!("Encoded input as {:?}", encoded);

View File

@ -18,6 +18,7 @@ use crate::selection::Selection;
use crate::shapecache::*;
use crate::tabbar::{TabBarItem, TabBarState};
use crate::termwindow::keyevent::KeyTableState;
use crate::termwindow::modal::Modal;
use ::wezterm_term::input::{ClickPosition, MouseButton as TMB};
use ::window::*;
use anyhow::{anyhow, ensure, Context};
@ -60,7 +61,9 @@ use wezterm_term::{Alert, StableRowIndex, TerminalConfiguration};
pub mod box_model;
pub mod clipboard;
mod keyevent;
pub mod modal;
mod mouseevent;
pub mod paneselect;
mod prevcursor;
mod render;
pub mod resize;
@ -382,6 +385,8 @@ pub struct TermWindow {
ui_items: Vec<UIItem>,
dragging: Option<(UIItem, MouseEvent)>,
modal: RefCell<Option<Rc<dyn Modal>>>,
event_states: HashMap<String, EventState>,
has_animation: RefCell<Option<Instant>>,
/// We use this to attempt to do something reasonable
@ -818,6 +823,7 @@ impl TermWindow {
last_ui_item: None,
is_click_to_focus_window: false,
key_table_state: KeyTableState::default(),
modal: RefCell::new(None),
};
let tw = Rc::new(RefCell::new(myself));
@ -887,6 +893,7 @@ impl TermWindow {
crate::update::start_update_checker();
front_end().record_known_window(window, mux_window_id);
Ok(())
}
@ -1005,6 +1012,7 @@ impl TermWindow {
match notif {
TermWindowNotif::InvalidateShapeCache => {
self.shape_cache.borrow_mut().clear();
self.invalidate_modal();
window.invalidate();
}
TermWindowNotif::PerformAssignment {
@ -1140,6 +1148,7 @@ impl TermWindow {
self.tab_state.borrow_mut().clear();
self.current_highlight.take();
self.invalidate_fancy_tab_bar();
self.invalidate_modal();
let mux = Mux::get().expect("to be main thread with mux running");
if let Some(window) = mux.get_window(self.mux_window_id) {
@ -1503,6 +1512,7 @@ impl TermWindow {
self.shape_cache.borrow_mut().clear();
self.fancy_tab_bar.take();
self.invalidate_fancy_tab_bar();
self.invalidate_modal();
self.input_map = InputMap::new(&config);
self.leader_is_down = None;
let dimensions = self.dimensions;
@ -1529,9 +1539,27 @@ impl TermWindow {
window.invalidate();
}
self.invalidate_modal();
self.emit_window_event("window-config-reloaded", None);
}
fn invalidate_modal(&mut self) {
if let Some(modal) = self.get_modal() {
modal.reconfigure(self);
}
}
pub fn cancel_modal(&self) {
self.modal.borrow_mut().take();
if let Some(window) = self.window.as_ref() {
window.invalidate();
}
}
fn get_modal(&self) -> Option<Rc<dyn Modal>> {
self.modal.borrow().as_ref().map(|m| Rc::clone(&m))
}
fn update_scrollbar(&mut self) {
if !self.show_scroll_bar {
return;
@ -1630,6 +1658,7 @@ impl TermWindow {
if new_tab_bar != self.tab_bar {
self.tab_bar = new_tab_bar;
self.invalidate_fancy_tab_bar();
self.invalidate_modal();
if let Some(window) = self.window.as_ref() {
window.invalidate();
}
@ -2043,6 +2072,12 @@ impl TermWindow {
) -> anyhow::Result<()> {
use KeyAssignment::*;
if let Some(modal) = self.get_modal() {
if modal.perform_assignment(assignment, self) {
return Ok(());
}
}
if pane.perform_assignment(assignment) {
return Ok(());
}
@ -2538,6 +2573,10 @@ impl TermWindow {
}),
);
}
PaneSelect(args) => {
let modal = crate::termwindow::paneselect::PaneSelector::new(self, args);
self.modal.borrow_mut().replace(Rc::new(modal));
}
};
Ok(())
}

View File

@ -0,0 +1,29 @@
use crate::termwindow::box_model::ComputedElement;
use crate::TermWindow;
use config::keyassignment::KeyAssignment;
use downcast_rs::{impl_downcast, Downcast};
use std::cell::Ref;
use wezterm_term::{KeyCode, KeyModifiers, MouseEvent};
pub trait Modal: Downcast {
fn perform_assignment(
&self,
_assignment: &KeyAssignment,
_term_window: &mut TermWindow,
) -> bool {
false
}
fn mouse_event(&self, event: MouseEvent, term_window: &mut TermWindow) -> anyhow::Result<()>;
fn key_down(
&self,
key: KeyCode,
mods: KeyModifiers,
term_window: &mut TermWindow,
) -> anyhow::Result<()>;
fn computed_element(
&self,
term_window: &mut TermWindow,
) -> anyhow::Result<Ref<[ComputedElement]>>;
fn reconfigure(&self, term_window: &mut TermWindow);
}
impl_downcast!(Modal);

View File

@ -0,0 +1,186 @@
use crate::color::LinearRgba;
use crate::termwindow::box_model::*;
use crate::termwindow::modal::Modal;
use crate::termwindow::DimensionContext;
use crate::utilsprites::RenderMetrics;
use crate::TermWindow;
use config::keyassignment::{KeyAssignment, PaneSelectArguments};
use mux::Mux;
use std::cell::{Ref, RefCell};
use wezterm_term::{KeyCode, KeyModifiers, MouseEvent};
pub struct PaneSelector {
element: RefCell<Option<Vec<ComputedElement>>>,
labels: RefCell<Vec<String>>,
selection: RefCell<String>,
alphabet: String,
}
impl PaneSelector {
pub fn new(term_window: &mut TermWindow, args: &PaneSelectArguments) -> Self {
let alphabet = if args.alphabet.is_empty() {
term_window.config.quick_select_alphabet.clone()
} else {
args.alphabet.clone()
};
Self {
element: RefCell::new(None),
labels: RefCell::new(vec![]),
selection: RefCell::new(String::new()),
alphabet,
}
}
fn compute(
term_window: &mut TermWindow,
alphabet: &str,
) -> anyhow::Result<(Vec<ComputedElement>, Vec<String>)> {
let font = term_window
.fonts
.pane_select_font()
.expect("to resolve pane selection font");
let metrics = RenderMetrics::with_font_metrics(&font.metrics());
let top_bar_height = if term_window.show_tab_bar && !term_window.config.tab_bar_at_bottom {
term_window.tab_bar_pixel_height().unwrap()
} else {
0.
};
let (padding_left, padding_top) = term_window.padding_left_top();
let border = term_window.get_os_border();
let top_pixel_y = top_bar_height + padding_top + border.top.get() as f32;
let panes = term_window.get_panes_to_render();
let labels =
crate::overlay::quickselect::compute_labels_for_alphabet(alphabet, panes.len());
let mut elements = vec![];
for pos in panes {
let caption = labels[pos.index].clone();
let element =
Element::new(&font, ElementContent::Text(caption)).colors(ElementColors {
border: BorderColor::default(),
bg: LinearRgba::with_srgba(0x00, 0x00, 0x00, 0xff).into(),
text: LinearRgba::with_srgba(0xff, 0xff, 0xff, 0xff).into(),
});
let dimensions = term_window.dimensions;
let pane_dims = pos.pane.get_dimensions();
let computed = term_window.compute_element(
&LayoutContext {
height: DimensionContext {
dpi: dimensions.dpi as f32,
pixel_max: dimensions.pixel_height as f32,
pixel_cell: metrics.cell_size.height as f32,
},
width: DimensionContext {
dpi: dimensions.dpi as f32,
pixel_max: dimensions.pixel_width as f32,
pixel_cell: metrics.cell_size.width as f32,
},
bounds: euclid::rect(
padding_left
+ ((pos.left as f32 + pane_dims.cols as f32 / 2.)
* term_window.render_metrics.cell_size.width as f32),
top_pixel_y
+ ((pos.top as f32 + pane_dims.viewport_rows as f32 / 2.)
* term_window.render_metrics.cell_size.height as f32),
pane_dims.cols as f32 * term_window.render_metrics.cell_size.width as f32,
pane_dims.viewport_rows as f32
* term_window.render_metrics.cell_size.height as f32,
),
metrics: &metrics,
gl_state: term_window.render_state.as_ref().unwrap(),
},
&element,
)?;
elements.push(computed);
}
Ok((elements, labels))
}
}
impl Modal for PaneSelector {
fn perform_assignment(
&self,
_assignment: &KeyAssignment,
_term_window: &mut TermWindow,
) -> bool {
false
}
fn mouse_event(&self, _event: MouseEvent, _term_window: &mut TermWindow) -> anyhow::Result<()> {
Ok(())
}
fn key_down(
&self,
key: KeyCode,
mods: KeyModifiers,
term_window: &mut TermWindow,
) -> anyhow::Result<()> {
match (key, mods) {
(KeyCode::Escape, KeyModifiers::NONE) => {
term_window.cancel_modal();
}
(KeyCode::Char(c), KeyModifiers::NONE) | (KeyCode::Char(c), KeyModifiers::SHIFT) => {
// Type to add to the selection
let mut selection = self.selection.borrow_mut();
selection.push(c);
// and if we have a complete match, activate that pane
if let Some(pane_index) = self.labels.borrow().iter().position(|s| s == &*selection)
{
let mux = Mux::get().unwrap();
let tab = match mux.get_active_tab_for_window(term_window.mux_window_id) {
Some(tab) => tab,
None => return Ok(()),
};
let tab_id = tab.tab_id();
if term_window.tab_state(tab_id).overlay.is_none() {
let panes = tab.iter_panes();
if panes.iter().position(|p| p.index == pane_index).is_some() {
tab.set_active_idx(pane_index);
}
}
term_window.cancel_modal();
}
}
(KeyCode::Backspace, KeyModifiers::NONE) => {
// Backspace to edit the selection
let mut selection = self.selection.borrow_mut();
selection.pop();
}
(KeyCode::Char('u'), KeyModifiers::CTRL) => {
// CTRL-u to clear the selection
let mut selection = self.selection.borrow_mut();
selection.clear();
}
_ => {}
}
Ok(())
}
fn computed_element(
&self,
term_window: &mut TermWindow,
) -> anyhow::Result<Ref<[ComputedElement]>> {
if self.element.borrow().is_none() {
let (element, labels) = Self::compute(term_window, &self.alphabet)?;
self.element.borrow_mut().replace(element);
*self.labels.borrow_mut() = labels;
}
Ok(Ref::map(self.element.borrow(), |v| {
v.as_ref().unwrap().as_slice()
}))
}
fn reconfigure(&self, _term_window: &mut TermWindow) {
self.element.borrow_mut().take();
}
}

View File

@ -221,6 +221,7 @@ impl super::TermWindow {
self.render_state.as_mut().unwrap().vb[vb_idx].need_more_quads()
{
self.invalidate_fancy_tab_bar();
self.invalidate_modal();
// Round up to next multiple of 1024 that is >=
// the number of needed quads for this frame
@ -263,6 +264,7 @@ impl super::TermWindow {
self.recreate_texture_atlas(Some(size))
};
self.invalidate_fancy_tab_bar();
self.invalidate_modal();
if let Err(err) = result {
if self.allow_images {
@ -283,6 +285,7 @@ impl super::TermWindow {
}
} else if err.root_cause().downcast_ref::<ClearShapeCache>().is_some() {
self.invalidate_fancy_tab_bar();
self.invalidate_modal();
self.shape_cache.borrow_mut().clear();
} else {
log::error!("paint_opengl_pass failed: {:#}", err);
@ -821,6 +824,24 @@ impl super::TermWindow {
Ok(computed)
}
fn paint_modal(&mut self) -> anyhow::Result<()> {
if let Some(modal) = self.get_modal() {
for computed in modal.computed_element(self)?.iter() {
let mut ui_items = computed.ui_items();
let gl_state = self.render_state.as_ref().unwrap();
let vb = &gl_state.vb[1];
let mut vb_mut = vb.current_vb_mut();
let mut layer1 = vb.map(&mut vb_mut);
self.render_element(&computed, &mut layer1, None)?;
self.ui_items.append(&mut ui_items);
}
}
Ok(())
}
fn paint_fancy_tab_bar(&self) -> anyhow::Result<Vec<UIItem>> {
let computed = self
.fancy_tab_bar
@ -1656,6 +1677,7 @@ impl super::TermWindow {
self.paint_tab_bar()?;
}
self.paint_modal()?;
self.paint_window_borders()?;
Ok(())

View File

@ -51,6 +51,9 @@ impl super::TermWindow {
} else {
self.scaling_changed(dimensions, self.fonts.get_font_scale(), window);
}
if let Some(modal) = self.get_modal() {
modal.reconfigure(self);
}
self.emit_window_event("window-resized", None);
}