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

Allow using unit like "1cell" or "5%" for window_padding

This commit introduces the `Dimension` type which allows specifying
a value in a variety of units; pixels, points, cells, percent.

`Dimension` needs contextual information to be evaluated as pixel
values, which makes resolving the value from the config slightly
more of a chore.

However, this type allows more flexible configurations that scale
with the font size and display dpi.

refs: #1124, #291, #195
This commit is contained in:
Wez Furlong 2021-10-07 19:26:22 -07:00
parent 9b4f7e78d6
commit 8c3477006f
9 changed files with 401 additions and 80 deletions

View File

@ -39,6 +39,7 @@ pub mod lua;
mod ssh;
mod terminal;
mod tls;
mod units;
mod unix;
mod version;
@ -52,6 +53,7 @@ pub use keys::*;
pub use ssh::*;
pub use terminal::*;
pub use tls::*;
pub use units::*;
pub use unix::*;
pub use version::*;
@ -1339,14 +1341,14 @@ impl DefaultCursorStyle {
#[derive(Default, Deserialize, Serialize, Clone, Copy, Debug)]
pub struct WindowPadding {
#[serde(default)]
pub left: u16,
#[serde(default)]
pub top: u16,
#[serde(default)]
pub right: u16,
#[serde(default)]
pub bottom: u16,
#[serde(default, deserialize_with = "de_pixels")]
pub left: Dimension,
#[serde(default, deserialize_with = "de_pixels")]
pub top: Dimension,
#[serde(default, deserialize_with = "de_pixels")]
pub right: Dimension,
#[serde(default, deserialize_with = "de_pixels")]
pub bottom: Dimension,
}
impl_lua_conversion!(WindowPadding);

188
config/src/units.rs Normal file
View File

@ -0,0 +1,188 @@
use serde::{Deserializer, Serialize, Serializer};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum DefaultUnit {
Points,
Pixels,
Percent,
Cells,
}
impl DefaultUnit {
fn to_dimension(self, value: f32) -> Dimension {
match self {
Self::Points => Dimension::Points(value),
Self::Pixels => Dimension::Pixels(value),
Self::Percent => Dimension::Percent(value / 100.),
Self::Cells => Dimension::Cells(value),
}
}
}
impl<'de> serde::de::Visitor<'de> for DefaultUnit {
type Value = Dimension;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("f64 or i64")
}
fn visit_f32<E>(self, value: f32) -> Result<Dimension, E>
where
E: serde::de::Error,
{
Ok(self.to_dimension(value))
}
fn visit_f64<E>(self, value: f64) -> Result<Dimension, E>
where
E: serde::de::Error,
{
Ok(self.to_dimension(value as f32))
}
fn visit_i64<E>(self, value: i64) -> Result<Dimension, E>
where
E: serde::de::Error,
{
Ok(self.to_dimension(value as f32))
}
fn visit_str<E>(self, s: &str) -> Result<Dimension, E>
where
E: serde::de::Error,
{
if let Ok(value) = s.parse::<f32>() {
Ok(self.to_dimension(value))
} else {
fn is_unit(s: &str, unit: &'static str) -> Option<f32> {
let s = s.strip_suffix(unit)?.trim();
s.parse().ok()
}
if let Some(v) = is_unit(s, "px") {
Ok(DefaultUnit::Pixels.to_dimension(v))
} else if let Some(v) = is_unit(s, "%") {
Ok(DefaultUnit::Percent.to_dimension(v))
} else if let Some(v) = is_unit(s, "pt") {
Ok(DefaultUnit::Points.to_dimension(v))
} else if let Some(v) = is_unit(s, "cell") {
Ok(DefaultUnit::Cells.to_dimension(v))
} else {
Err(serde::de::Error::custom(format!(
"expected either a number or a string of \
the form '123px' where 'px' is a unit and \
can be one of 'px', '%', 'pt' or 'cell', \
but got {}",
s
)))
}
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum Dimension {
/// A value expressed in points, where 72 points == 1 inch.
Points(f32),
/// A value expressed in raw pixels
Pixels(f32),
/// A value expressed in terms of a fraction of the maximum
/// value in the same direction. For example, left padding
/// of 10% depends on the pixel width of that element.
/// The value is 1.0 == 100%. It is possible to express
/// eg: 2.0 for 200%.
Percent(f32),
/// A value expressed in terms of a fraction of the cell
/// size computed from the configured font size.
/// 1.0 == the cell size.
Cells(f32),
}
impl Dimension {
pub fn is_zero(&self) -> bool {
match self {
Self::Points(n) | Self::Pixels(n) | Self::Percent(n) | Self::Cells(n) => *n == 0.,
}
}
}
impl Default for Dimension {
fn default() -> Self {
Self::Pixels(0.)
}
}
impl Serialize for Dimension {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = match self {
Self::Points(n) => format!("{}pt", n),
Self::Pixels(n) => format!("{}px", n),
Self::Percent(n) => format!("{}%", n * 100.),
Self::Cells(n) => format!("{}cell", n),
};
serializer.serialize_str(&s)
}
}
#[derive(Clone, Copy, Debug)]
pub struct DimensionContext {
pub dpi: f32,
/// Width/Height or other upper bound on the dimension,
/// measured in pixels.
pub pixel_max: f32,
/// Width/Height of the font metrics cell size in the appropriate
/// dimension, measured in pixels.
pub pixel_cell: f32,
}
impl Dimension {
pub fn evaluate_as_pixels(&self, context: DimensionContext) -> f32 {
match self {
Self::Pixels(n) => *n,
Self::Points(pt) => pt * context.dpi / 72.0,
Self::Percent(p) => p * context.pixel_max,
Self::Cells(c) => c * context.pixel_cell,
}
}
}
fn de_dimension<'de, D>(unit: DefaultUnit, deserializer: D) -> Result<Dimension, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(unit)
}
pub fn de_pixels<'de, D>(deserializer: D) -> Result<Dimension, D::Error>
where
D: Deserializer<'de>,
{
de_dimension(DefaultUnit::Pixels, deserializer)
}
pub fn de_points<'de, D>(deserializer: D) -> Result<Dimension, D::Error>
where
D: Deserializer<'de>,
{
de_dimension(DefaultUnit::Points, deserializer)
}
pub fn de_percent<'de, D>(deserializer: D) -> Result<Dimension, D::Error>
where
D: Deserializer<'de>,
{
de_dimension(DefaultUnit::Percent, deserializer)
}
pub fn de_cells<'de, D>(deserializer: D) -> Result<Dimension, D::Error>
where
D: Deserializer<'de>,
{
de_dimension(DefaultUnit::Cells, deserializer)
}

View File

@ -57,6 +57,7 @@ As features stabilize some brief notes about them will accumulate here.
* Fixed: ssh config parser incorrectly split `Host` patterns with commas instead of whitespace [#1196](https://github.com/wez/wezterm/issues/1196)
* Fixed: search now auto-updates when the pane content changes [#1205](https://github.com/wez/wezterm/issues/1205)
* Fixed: fonts with emoji presentation are shifted to better align with the primary font baseline [#1203](https://github.com/wez/wezterm/issues/1203)
* New: [window_padding](config/lua/config/window_padding.md) now accepts values such as `"1cell"` or `"30%"` to compute values based on font or window metrics.
### 20210814-124438-54e29167

View File

@ -227,20 +227,9 @@ return {
### Window Padding
You may add padding around the edges of the terminal cells:
You may add padding around the edges of the terminal area.
```lua
return {
window_padding = {
left = 2,
-- This will become the scrollbar width if you have enabled the scrollbar!
right = 2,
top = 0,
bottom = 0,
}
}
```
[See the window_padding docs for more info](lua/config/window_padding.md)
## Styling Inactive Panes

View File

@ -0,0 +1,46 @@
# window_padding
Controls the amount of padding between the window border and the
terminal cells.
Padding is measured in pixels.
If [enable_scroll_bar](enable_scroll_bar.md) is `true`, then the value you
set for `right` will control the width of the scrollbar. If you have
enabled the scrollbar and have set `right` to `0` then the right padding
(and thus the scrollbar width) will instead match the width of a cell.
```lua
return {
window_padding = {
left = 2,
right = 2,
top = 0,
bottom = 0,
}
}
```
*Since: nightly builds only*
You may now express padding using a number of different units by specifying
a string value with a unit suffix:
* `"1px"` - the `px` suffix indicates pixels, so this represents a `1` pixel value
* `"1pt"` - the `pt` suffix indicates points. There are `72` points in `1 inch`. The actual size this occupies on screen depends on the dpi of the display device.
* `"1cell"` - the `cell` suffix indicates the size of the terminal cell, which in turn depends on the font size, font scaling and dpi. When used for width, the width of the cell is used. When used for height, the height of the cell is used.
* `"1%"` - the `%` suffix indicates the size of the terminal portion of the display, which is computed based on the number of rows/columns and the size of the cell. While it is possible to specify percentage, there are some resize scenarios where the percentage value may not be 100% stable/deterministic, as the size of the padding is used to compute the number of rows/columns.
You may use a fractional number such as `"0.5cell"` or numbers large than one such as `"72pt"`.
```lua
return {
window_padding = {
left = "1cell",
right = "1cell",
top = "0.5cell",
bottom = "0.5cell",
}
}
```

View File

@ -22,7 +22,7 @@ use config::keyassignment::{
ClipboardCopyDestination, ClipboardPasteSource, InputMap, KeyAssignment, SpawnCommand,
};
use config::{
configuration, AudibleBell, ConfigHandle, GradientOrientation, TermConfig,
configuration, AudibleBell, ConfigHandle, DimensionContext, GradientOrientation, TermConfig,
WindowCloseConfirmation,
};
use luahelper::impl_lua_conversion;
@ -550,14 +550,26 @@ impl TermWindow {
pixel_height: (render_metrics.cell_size.height as usize * physical_rows) as u16,
};
let h_context = DimensionContext {
dpi: dpi as f32,
pixel_max: terminal_size.pixel_width as f32,
pixel_cell: render_metrics.cell_size.width as f32,
};
let padding_left = config.window_padding.left.evaluate_as_pixels(h_context) as u16;
let padding_right = resize::effective_right_padding(&config, h_context);
let v_context = DimensionContext {
dpi: dpi as f32,
pixel_max: terminal_size.pixel_height as f32,
pixel_cell: render_metrics.cell_size.height as f32,
};
let padding_top = config.window_padding.top.evaluate_as_pixels(v_context) as u16;
let padding_bottom = config.window_padding.bottom.evaluate_as_pixels(v_context) as u16;
let dimensions = Dimensions {
pixel_width: (terminal_size.pixel_width
+ config.window_padding.left
+ resize::effective_right_padding(&config, &render_metrics))
as usize,
pixel_width: (terminal_size.pixel_width + padding_left + padding_right) as usize,
pixel_height: ((terminal_size.rows * render_metrics.cell_size.height as u16)
+ config.window_padding.top
+ config.window_padding.bottom) as usize
+ padding_top
+ padding_bottom) as usize
+ tab_bar_height,
dpi,
};
@ -1263,10 +1275,23 @@ impl TermWindow {
let active_tab = tabs.iter().find(|t| t.is_active).cloned();
let active_pane = panes.iter().find(|p| p.is_active).cloned();
let v_context = DimensionContext {
dpi: self.dimensions.dpi as f32,
pixel_max: self.terminal_size.pixel_height as f32,
pixel_cell: self.render_metrics.cell_size.height as f32,
};
let padding_top = self.config.window_padding.top.evaluate_as_pixels(v_context) as u16;
let padding_bottom = self
.config
.window_padding
.bottom
.evaluate_as_pixels(v_context) as u16;
let tab_bar_y = if self.config.tab_bar_at_bottom {
let avail_height = self.dimensions.pixel_height.saturating_sub(
(self.config.window_padding.top + self.config.window_padding.bottom) as usize,
);
let avail_height = self
.dimensions
.pixel_height
.saturating_sub((padding_top + padding_bottom) as usize);
let num_rows = avail_height as usize / self.render_metrics.cell_size.height as usize;
@ -1396,14 +1421,16 @@ impl TermWindow {
fn update_text_cursor(&mut self, pane: &Rc<dyn Pane>) {
let cursor = pane.get_cursor_position();
if let Some(win) = self.window.as_ref() {
let config = &self.config;
let top = pane.get_dimensions().physical_top + if self.show_tab_bar { -1 } else { 0 };
let (padding_left, padding_top) = self.padding_left_top();
let r = Rect::new(
Point::new(
(cursor.x.max(0) as isize * self.render_metrics.cell_size.width)
.add(config.window_padding.left as isize),
.add(padding_left as isize),
((cursor.y - top).max(0) as isize * self.render_metrics.cell_size.height)
.add(config.window_padding.top as isize),
.add(padding_top as isize),
),
self.render_metrics.cell_size,
);

View File

@ -58,25 +58,23 @@ impl super::TermWindow {
self.current_mouse_event.replace(event.clone());
let config = &self.config;
let first_line_offset = if self.show_tab_bar && !self.config.tab_bar_at_bottom {
self.tab_bar_pixel_height().unwrap_or(0.) as isize
} else {
0
};
let (padding_left, padding_top) = self.padding_left_top();
let y = (event
.coords
.y
.sub(config.window_padding.top as isize)
.sub(padding_top as isize)
.sub(first_line_offset)
.max(0)
/ self.render_metrics.cell_size.height) as i64;
let x = (event
.coords
.x
.sub(config.window_padding.left as isize)
.max(0) as f32)
let x = (event.coords.x.sub(padding_left as isize).max(0) as f32)
/ self.render_metrics.cell_size.width as f32;
let x = if !pane.is_mouse_grabbed() {
// Round the x coordinate so that we're a bit more forgiving of

View File

@ -18,7 +18,9 @@ use ::window::glium::uniforms::{
use ::window::glium::{uniform, BlendingFunction, LinearBlendingFactor, Surface};
use ::window::{Point, Rect, Size, WindowOps};
use anyhow::anyhow;
use config::{ConfigHandle, HsbTransform, TabBarColors, TextStyle, VisualBellTarget};
use config::{
ConfigHandle, DimensionContext, HsbTransform, TabBarColors, TextStyle, VisualBellTarget,
};
use mux::pane::Pane;
use mux::renderable::{RenderableDimensions, StableCursorPosition};
use mux::tab::{PositionedPane, PositionedSplit, SplitDirection};
@ -807,12 +809,14 @@ impl super::TermWindow {
let config = &self.config;
let palette = pos.pane.palette();
let (padding_left, padding_top) = self.padding_left_top();
let tab_bar_height = if self.show_tab_bar && !self.config.tab_bar_at_bottom {
self.tab_bar_pixel_height()?
} else {
0.
};
let top_pixel_y = tab_bar_height + self.config.window_padding.top as f32;
let top_pixel_y = tab_bar_height + padding_top;
let cursor = pos.pane.get_cursor_position();
if pos.is_active {
@ -935,12 +939,8 @@ impl super::TermWindow {
&mut layers[0],
Rect::new(
Point::new(
((pos.left as f32 * cell_width) + self.config.window_padding.left as f32)
as isize,
(top_pixel_y
+ (pos.top as f32 * cell_height)
+ self.config.window_padding.top as f32)
as isize,
((pos.left as f32 * cell_width) + padding_left) as isize,
(top_pixel_y + (pos.top as f32 * cell_height) + padding_top) as isize,
),
Size::new(
(pos.width as f32 * cell_width) as isize,
@ -1002,13 +1002,8 @@ impl super::TermWindow {
&mut layers[0],
Rect::new(
Point::new(
((pos.left as f32 * cell_width)
+ self.config.window_padding.left as f32)
as isize,
(top_pixel_y
+ (pos.top as f32 * cell_height)
+ self.config.window_padding.top as f32)
as isize,
((pos.left as f32 * cell_width) + padding_left) as isize,
(top_pixel_y + (pos.top as f32 * cell_height) + padding_top) as isize,
),
Size::new(
(pos.width as f32 * cell_width) as isize,
@ -1104,7 +1099,7 @@ impl super::TermWindow {
RenderScreenLineOpenGLParams {
top_pixel_y: top_pixel_y
+ (line_idx + pos.top) as f32 * self.render_metrics.cell_size.height as f32,
left_pixel_x: self.config.window_padding.left as f32
left_pixel_x: padding_left
+ (pos.left as f32 * self.render_metrics.cell_size.width as f32),
stable_line_idx: Some(stable_row),
line: &line,
@ -1245,6 +1240,28 @@ impl super::TermWindow {
Ok(())
}
pub fn padding_left_top(&self) -> (f32, f32) {
let h_context = DimensionContext {
dpi: self.dimensions.dpi as f32,
pixel_max: self.terminal_size.pixel_width as f32,
pixel_cell: self.render_metrics.cell_size.width as f32,
};
let v_context = DimensionContext {
dpi: self.dimensions.dpi as f32,
pixel_max: self.terminal_size.pixel_height as f32,
pixel_cell: self.render_metrics.cell_size.height as f32,
};
let padding_left = self
.config
.window_padding
.left
.evaluate_as_pixels(h_context);
let padding_top = self.config.window_padding.top.evaluate_as_pixels(v_context);
(padding_left, padding_top)
}
pub fn paint_split_opengl(
&mut self,
split: &PositionedSplit,
@ -1285,13 +1302,15 @@ impl super::TermWindow {
quad.set_texture_adjust(0., 0., 0., 0.);
quad.set_has_color(false);
let (padding_left, padding_top) = self.padding_left_top();
let pos_y = (self.dimensions.pixel_height as f32 / -2.)
+ split.top as f32 * cell_height
+ first_row_offset
+ self.config.window_padding.top as f32;
+ padding_top;
let pos_x = (self.dimensions.pixel_width as f32 / -2.)
+ split.left as f32 * cell_width
+ self.config.window_padding.left as f32;
+ padding_left;
if split.direction == SplitDirection::Horizontal {
quad.set_position(
@ -1301,9 +1320,9 @@ impl super::TermWindow {
pos_y + split.size as f32 * cell_height,
);
self.ui_items.push(UIItem {
x: self.config.window_padding.left as usize + (split.left * cell_width as usize),
x: padding_left as usize + (split.left * cell_width as usize),
width: cell_width as usize,
y: self.config.window_padding.top as usize
y: padding_top as usize
+ first_row_offset as usize
+ split.top * cell_height as usize,
height: split.size * cell_height as usize,
@ -1317,9 +1336,9 @@ impl super::TermWindow {
pos_y + cell_height,
);
self.ui_items.push(UIItem {
x: self.config.window_padding.left as usize + (split.left * cell_width as usize),
x: padding_left as usize + (split.left * cell_width as usize),
width: split.size * cell_width as usize,
y: self.config.window_padding.top as usize
y: padding_top as usize
+ first_row_offset as usize
+ split.top * cell_height as usize,
height: cell_height as usize,

View File

@ -1,6 +1,6 @@
use crate::utilsprites::RenderMetrics;
use ::window::{Dimensions, Window, WindowOps, WindowState};
use config::ConfigHandle;
use config::{ConfigHandle, DimensionContext};
use mux::Mux;
use portable_pty::PtySize;
use std::rc::Rc;
@ -138,12 +138,27 @@ impl super::TermWindow {
let rows = size.rows;
let cols = size.cols;
let h_context = DimensionContext {
dpi: dimensions.dpi as f32,
pixel_max: size.pixel_width as f32,
pixel_cell: self.render_metrics.cell_size.width as f32,
};
let v_context = DimensionContext {
dpi: dimensions.dpi as f32,
pixel_max: size.pixel_height as f32,
pixel_cell: self.render_metrics.cell_size.height as f32,
};
let padding_left = config.window_padding.left.evaluate_as_pixels(h_context) as u16;
let padding_top = config.window_padding.top.evaluate_as_pixels(v_context) as u16;
let padding_bottom = config.window_padding.bottom.evaluate_as_pixels(v_context) as u16;
let padding_right = effective_right_padding(&config, h_context);
let pixel_height = (rows * self.render_metrics.cell_size.height as u16)
+ (config.window_padding.top + config.window_padding.bottom)
+ (padding_top + padding_bottom)
+ tab_bar_height as u16;
let pixel_width = (cols * self.render_metrics.cell_size.width as u16)
+ (config.window_padding.left + self.effective_right_padding(&config));
+ (padding_left + padding_right);
let dims = Dimensions {
pixel_width: pixel_width as usize,
@ -154,12 +169,28 @@ impl super::TermWindow {
(size, dims)
} else {
// Resize of the window dimensions may result in changed terminal dimensions
let avail_width = dimensions.pixel_width.saturating_sub(
(config.window_padding.left + self.effective_right_padding(&config)) as usize,
);
let h_context = DimensionContext {
dpi: dimensions.dpi as f32,
pixel_max: self.terminal_size.pixel_width as f32,
pixel_cell: self.render_metrics.cell_size.width as f32,
};
let v_context = DimensionContext {
dpi: dimensions.dpi as f32,
pixel_max: self.terminal_size.pixel_height as f32,
pixel_cell: self.render_metrics.cell_size.height as f32,
};
let padding_left = config.window_padding.left.evaluate_as_pixels(h_context) as u16;
let padding_top = config.window_padding.top.evaluate_as_pixels(v_context) as u16;
let padding_bottom = config.window_padding.bottom.evaluate_as_pixels(v_context) as u16;
let padding_right = effective_right_padding(&config, h_context);
let avail_width = dimensions
.pixel_width
.saturating_sub((padding_left + padding_right) as usize);
let avail_height = dimensions
.pixel_height
.saturating_sub((config.window_padding.top + config.window_padding.bottom) as usize)
.saturating_sub((padding_top + padding_bottom) as usize)
.saturating_sub(tab_bar_height as usize);
let rows = avail_height / self.render_metrics.cell_size.height as usize;
@ -306,14 +337,27 @@ impl super::TermWindow {
0
};
let h_context = DimensionContext {
dpi: self.dimensions.dpi as f32,
pixel_max: self.dimensions.pixel_width as f32,
pixel_cell: render_metrics.cell_size.width as f32,
};
let v_context = DimensionContext {
dpi: self.dimensions.dpi as f32,
pixel_max: self.dimensions.pixel_height as f32,
pixel_cell: render_metrics.cell_size.height as f32,
};
let padding_left = config.window_padding.left.evaluate_as_pixels(h_context) as u16;
let padding_top = config.window_padding.top.evaluate_as_pixels(v_context) as u16;
let padding_bottom = config.window_padding.bottom.evaluate_as_pixels(v_context) as u16;
let dimensions = Dimensions {
pixel_width: ((terminal_size.cols * render_metrics.cell_size.width as u16)
+ config.window_padding.left
+ effective_right_padding(&config, &render_metrics))
as usize,
+ padding_left
+ effective_right_padding(&config, h_context)) as usize,
pixel_height: ((terminal_size.rows * render_metrics.cell_size.height as u16)
+ config.window_padding.top
+ config.window_padding.bottom) as usize
+ padding_top
+ padding_bottom) as usize
+ tab_bar_height,
dpi: config.dpi.unwrap_or_else(|| ::window::default_dpi()) as usize,
};
@ -331,7 +375,14 @@ impl super::TermWindow {
}
pub fn effective_right_padding(&self, config: &ConfigHandle) -> u16 {
effective_right_padding(config, &self.render_metrics)
effective_right_padding(
config,
DimensionContext {
pixel_cell: self.render_metrics.cell_size.width as f32,
dpi: self.dimensions.dpi as f32,
pixel_max: self.dimensions.pixel_width as f32,
},
)
}
}
@ -339,10 +390,10 @@ impl super::TermWindow {
/// This is needed because the default is 0, but if the user has
/// enabled the scroll bar then they will expect it to have a reasonable
/// size unless they've specified differently.
pub fn effective_right_padding(config: &ConfigHandle, render_metrics: &RenderMetrics) -> u16 {
if config.enable_scroll_bar && config.window_padding.right == 0 {
render_metrics.cell_size.width as u16
pub fn effective_right_padding(config: &ConfigHandle, context: DimensionContext) -> u16 {
if config.enable_scroll_bar && config.window_padding.right.is_zero() {
context.pixel_cell as u16
} else {
config.window_padding.right as u16
config.window_padding.right.evaluate_as_pixels(context) as u16
}
}