1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-24 05:42:03 +03:00

notionally allow multiple background image layers

This adds some types that will enable richer background images.

* Can specify multiple layers
* Each layer can select from image files or gradient definitions
* Layers have additional properties to specify positioning, scaling,
  tiling and whether they scroll with the viewport.

None of the additional properties are hooked up yet.
This commit is contained in:
Wez Furlong 2022-05-29 10:30:36 -07:00
parent a9612bc613
commit 5d44ed6f85
5 changed files with 416 additions and 188 deletions

View File

@ -1,6 +1,185 @@
use crate::{default_one_point_oh, Config, HsbTransform};
use luahelper::impl_lua_conversion_dynamic;
use wezterm_dynamic::{FromDynamic, ToDynamic};
#[derive(Debug, Clone, FromDynamic, ToDynamic)]
pub enum BackgroundSource {
Gradient(Gradient),
File(String),
}
#[derive(Debug, Clone, FromDynamic, ToDynamic)]
pub struct BackgroundLayer {
pub source: BackgroundSource,
/// Where the top left corner of the background begins
#[dynamic(default)]
pub origin: BackgroundOrigin,
#[dynamic(default)]
pub attachment: BackgroundAttachment,
#[dynamic(default)]
pub repeat_x: BackgroundRepeat,
#[dynamic(default)]
pub repeat_y: BackgroundRepeat,
#[dynamic(default)]
pub vertical_align: BackgroundVerticalAlignment,
#[dynamic(default)]
pub horizontal_align: BackgroundHorizontalAlignment,
/// Additional alpha modifier
#[dynamic(default = "default_one_point_oh")]
pub opacity: f32,
/// Additional hsb transform
#[dynamic(default)]
pub hsb: HsbTransform,
#[dynamic(default)]
pub width: BackgroundSize,
#[dynamic(default)]
pub height: BackgroundSize,
}
impl BackgroundLayer {
pub fn with_legacy(cfg: &Config) -> Option<Self> {
let source = if let Some(gradient) = &cfg.window_background_gradient {
BackgroundSource::Gradient(gradient.clone())
} else if let Some(path) = &cfg.window_background_image {
BackgroundSource::File(path.to_string_lossy().to_string())
} else {
return None;
};
Some(BackgroundLayer {
source,
opacity: cfg.window_background_opacity,
hsb: cfg.window_background_image_hsb.unwrap_or_default(),
origin: Default::default(),
attachment: Default::default(),
repeat_x: Default::default(),
repeat_y: Default::default(),
vertical_align: Default::default(),
horizontal_align: Default::default(),
width: BackgroundSize::Percent(100),
height: BackgroundSize::Percent(100),
})
}
}
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/background-size>
#[derive(Debug, Copy, Clone, FromDynamic, ToDynamic)]
pub enum BackgroundSize {
/// Scales image as large as possible without cropping or stretching.
/// If the container is larger than the image, tiles the image unless
/// the correspond `repeat` is NoRepeat.
Contain,
/// Scale the image (preserving aspect ratio) to the smallest possible
/// size to the fill the container leaving no empty space.
/// If the aspect ratio differs from the background, the image is
/// cropped.
Cover,
/// Scales the image so that its aspect ratio is maintained
Auto,
/// Stretches the image to the specified length in pixels
Length(usize),
/// Stretches the image to a percentage of the background size
/// as determined by the `origin` property.
Percent(u8),
}
impl Default for BackgroundSize {
fn default() -> Self {
Self::Contain
}
}
#[derive(Debug, Copy, Clone, FromDynamic, ToDynamic)]
pub enum BackgroundHorizontalAlignment {
Left,
Center,
Right,
}
impl Default for BackgroundHorizontalAlignment {
fn default() -> Self {
Self::Left
}
}
#[derive(Debug, Copy, Clone, FromDynamic, ToDynamic)]
pub enum BackgroundVerticalAlignment {
Top,
Middle,
Bottom,
}
impl Default for BackgroundVerticalAlignment {
fn default() -> Self {
Self::Top
}
}
#[derive(Debug, Copy, Clone, FromDynamic, ToDynamic)]
pub enum BackgroundRepeat {
/// Repeat as much as possible to cover the area.
/// The last image will be clipped if it doesn't fit.
Repeat,
/// Repeat as much as possible without clipping.
/// The first and last images are aligned with the edges,
/// with any gaps being distributed evenly between
/// the images.
/// The `position` property is ignored unless only
/// a single image an be displayed without clipping.
/// Clipping will only occur when there isn't enough
/// room to display a single image.
Space,
/// As the available space increases, the images will
/// stretch until there is room (space >= 50% of image
/// size) for another one to be added. When adding a
/// new image, the current images compress to allow
/// room.
Round,
/// The image is not repeated.
/// The position of the image is defined by the
/// `position` property
NoRepeat,
}
impl Default for BackgroundRepeat {
fn default() -> Self {
Self::NoRepeat
}
}
#[derive(Debug, Copy, Clone, FromDynamic, ToDynamic)]
pub enum BackgroundAttachment {
Fixed,
Scroll,
}
impl Default for BackgroundAttachment {
fn default() -> Self {
Self::Fixed
}
}
#[derive(Debug, Copy, Clone, FromDynamic, ToDynamic)]
pub enum BackgroundOrigin {
BorderBox,
PaddingBox,
}
impl Default for BackgroundOrigin {
fn default() -> Self {
Self::BorderBox
}
}
#[derive(Debug, Copy, Clone, FromDynamic, ToDynamic)]
pub enum Interpolation {
Linear,

View File

@ -1,4 +1,4 @@
use crate::background::Gradient;
use crate::background::{BackgroundLayer, Gradient};
use crate::bell::{AudibleBell, EasingFunction, VisualBell};
use crate::color::{
ColorSchemeFile, HsbTransform, Palette, SrgbaTuple, TabBarStyle, WindowFrameConfig,
@ -391,6 +391,9 @@ pub struct Config {
#[dynamic(default)]
pub foreground_text_hsb: HsbTransform,
#[dynamic(default)]
pub background: Vec<BackgroundLayer>,
/// Specifies the alpha value to use when rendering the background
/// of the window. The background is taken either from the
/// window_background_image, or if there is none, the background
@ -962,6 +965,10 @@ impl Config {
}
}
if let Some(bg) = BackgroundLayer::with_legacy(self) {
cfg.background.push(bg);
}
cfg
}

View File

@ -0,0 +1,197 @@
use crate::Dimensions;
use anyhow::Context;
use config::{
BackgroundLayer, BackgroundSize, BackgroundSource, ConfigHandle, GradientOrientation,
};
use std::collections::HashMap;
use std::sync::Arc;
use termwiz::image::{ImageData, ImageDataType};
pub struct LoadedBackgroundLayer {
pub source: Arc<ImageData>,
pub def: BackgroundLayer,
}
fn load_background_layer(
layer: &BackgroundLayer,
dimensions: &Dimensions,
) -> anyhow::Result<LoadedBackgroundLayer> {
let data = match &layer.source {
BackgroundSource::Gradient(g) => {
let grad = g
.build()
.with_context(|| format!("building gradient {:?}", g))?;
let mut width = match layer.width {
BackgroundSize::Percent(p) => (p as u32 * dimensions.pixel_width as u32) / 100,
BackgroundSize::Length(u) => u as u32,
unsup => anyhow::bail!("{:?} not yet implemented", unsup),
};
let mut height = match layer.height {
BackgroundSize::Percent(p) => (p as u32 * dimensions.pixel_height as u32) / 100,
BackgroundSize::Length(u) => u as u32,
unsup => anyhow::bail!("{:?} not yet implemented", unsup),
};
if matches!(g.orientation, GradientOrientation::Radial { .. }) {
// To simplify the math, we compute a perfect circle
// for the radial gradient, and let the texture sampler
// perturb it to fill the window
width = width.min(height);
height = height.min(width);
}
let mut imgbuf = image::RgbaImage::new(width, height);
let fw = width as f64;
let fh = height as f64;
fn to_pixel(c: colorgrad::Color) -> image::Rgba<u8> {
let (r, g, b, a) = c.rgba_u8();
image::Rgba([r, g, b, a])
}
// Map t which is in range [a, b] to range [c, d]
fn remap(t: f64, a: f64, b: f64, c: f64, d: f64) -> f64 {
(t - a) * ((d - c) / (b - a)) + c
}
let (dmin, dmax) = grad.domain();
let rng = fastrand::Rng::new();
// We add some randomness to the position that we use to
// index into the color gradient, so that we can avoid
// visible color banding. The default 64 was selected
// because it it was the smallest value on my mac where
// the banding wasn't obvious.
let noise_amount = g.noise.unwrap_or_else(|| {
if matches!(g.orientation, GradientOrientation::Radial { .. }) {
16
} else {
64
}
});
fn noise(rng: &fastrand::Rng, noise_amount: usize) -> f64 {
if noise_amount == 0 {
0.
} else {
rng.usize(0..noise_amount) as f64 * -1.
}
}
match g.orientation {
GradientOrientation::Horizontal => {
for (x, _, pixel) in imgbuf.enumerate_pixels_mut() {
*pixel = to_pixel(grad.at(remap(
x as f64 + noise(&rng, noise_amount),
0.0,
fw,
dmin,
dmax,
)));
}
}
GradientOrientation::Vertical => {
for (_, y, pixel) in imgbuf.enumerate_pixels_mut() {
*pixel = to_pixel(grad.at(remap(
y as f64 + noise(&rng, noise_amount),
0.0,
fh,
dmin,
dmax,
)));
}
}
GradientOrientation::Linear { angle } => {
let angle = angle.unwrap_or(0.0).to_radians();
for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
let (x, y) = (x as f64, y as f64);
let (x, y) = (x - fw / 2., y - fh / 2.);
let t = x * f64::cos(angle) - y * f64::sin(angle);
*pixel = to_pixel(grad.at(remap(
t + noise(&rng, noise_amount),
-fw / 2.,
fw / 2.,
dmin,
dmax,
)));
}
}
GradientOrientation::Radial { radius, cx, cy } => {
let radius = fw * radius.unwrap_or(0.5);
let cx = fw * cx.unwrap_or(0.5);
let cy = fh * cy.unwrap_or(0.5);
for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
let nx = noise(&rng, noise_amount);
let ny = noise(&rng, noise_amount);
let t = (nx + (x as f64 - cx).powi(2) + (ny + y as f64 - cy).powi(2))
.sqrt()
/ radius;
*pixel = to_pixel(grad.at(t));
}
}
}
let data = imgbuf.into_vec();
ImageData::with_data(ImageDataType::new_single_frame(width, height, data))
}
BackgroundSource::File(path) => {
let data = std::fs::read(path)
.with_context(|| format!("Failed to load window_background_image {}", path))?;
log::info!("loaded {}", path);
let data = ImageDataType::EncodedFile(data).decode();
ImageData::with_data(data)
}
};
Ok(LoadedBackgroundLayer {
source: Arc::new(data),
def: layer.clone(),
})
}
pub fn load_background_image(
config: &ConfigHandle,
dimensions: &Dimensions,
) -> Vec<LoadedBackgroundLayer> {
let mut layers = vec![];
for layer in &config.background {
match load_background_layer(layer, dimensions) {
Ok(layer) => layers.push(layer),
Err(err) => {
log::error!("Failed to load background: {:#}", err);
}
}
}
layers
}
pub fn reload_background_image(
config: &ConfigHandle,
existing: &[LoadedBackgroundLayer],
dimensions: &Dimensions,
) -> Vec<LoadedBackgroundLayer> {
// We want to reuse the existing version of the image where possible
// so that the textures we may have cached can be re-used and so that
// animation state can be preserved across the reload.
let map: HashMap<_, _> = existing
.iter()
.map(|layer| (layer.source.hash(), &layer.source))
.collect();
load_background_image(config, dimensions)
.into_iter()
.map(|mut layer| {
let hash = layer.source.hash();
if let Some(existing) = map.get(&hash) {
layer.source = Arc::clone(existing);
}
layer
})
.collect()
}

View File

@ -17,6 +17,9 @@ use crate::scrollbar::*;
use crate::selection::Selection;
use crate::shapecache::*;
use crate::tabbar::{TabBarItem, TabBarState};
use crate::termwindow::background::{
load_background_image, reload_background_image, LoadedBackgroundLayer,
};
use crate::termwindow::keyevent::KeyTableState;
use crate::termwindow::modal::Modal;
use ::wezterm_term::input::{ClickPosition, MouseButton as TMB};
@ -27,8 +30,8 @@ use config::keyassignment::{
QuickSelectArguments, RotationDirection, SpawnCommand, SplitSize,
};
use config::{
configuration, AudibleBell, ConfigHandle, Dimension, DimensionContext, GradientOrientation,
TermConfig, WindowCloseConfirmation,
configuration, AudibleBell, ConfigHandle, Dimension, DimensionContext, TermConfig,
WindowCloseConfirmation,
};
use mlua::{FromLua, UserData, UserDataFields};
use mux::pane::{CloseReason, Pane, PaneId, Pattern as MuxPattern};
@ -50,7 +53,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use termwiz::hyperlink::Hyperlink;
use termwiz::image::{ImageData, ImageDataType};
use termwiz::surface::SequenceNo;
use wezterm_font::FontConfiguration;
use wezterm_gui_subcommands::GuiPosition;
@ -58,6 +60,7 @@ use wezterm_term::color::ColorPalette;
use wezterm_term::input::LastMouseClick;
use wezterm_term::{Alert, StableRowIndex, TerminalConfiguration};
pub mod background;
pub mod box_model;
pub mod clipboard;
mod keyevent;
@ -361,7 +364,7 @@ pub struct TermWindow {
pane_state: RefCell<HashMap<PaneId, PaneState>>,
semantic_zones: HashMap<PaneId, SemanticZoneCache>,
window_background: Option<Arc<ImageData>>,
window_background: Vec<LoadedBackgroundLayer>,
current_mouse_buttons: Vec<MousePress>,
current_mouse_capture: Option<MouseCapture>,
@ -517,165 +520,6 @@ impl TermWindow {
}
}
fn load_background_image(config: &ConfigHandle, dimensions: &Dimensions) -> Option<Arc<ImageData>> {
match &config.window_background_gradient {
Some(g) => match g.build() {
Ok(grad) => {
let mut width = dimensions.pixel_width as u32;
let mut height = dimensions.pixel_height as u32;
if matches!(g.orientation, GradientOrientation::Radial { .. }) {
// To simplify the math, we compute a perfect circle
// for the radial gradient, and let the texture sampler
// perturb it to fill the window
width = width.min(height);
height = height.min(width);
}
let mut imgbuf = image::RgbaImage::new(width, height);
let fw = width as f64;
let fh = height as f64;
fn to_pixel(c: colorgrad::Color) -> image::Rgba<u8> {
let (r, g, b, a) = c.rgba_u8();
image::Rgba([r, g, b, a])
}
// Map t which is in range [a, b] to range [c, d]
fn remap(t: f64, a: f64, b: f64, c: f64, d: f64) -> f64 {
(t - a) * ((d - c) / (b - a)) + c
}
let (dmin, dmax) = grad.domain();
let rng = fastrand::Rng::new();
// We add some randomness to the position that we use to
// index into the color gradient, so that we can avoid
// visible color banding. The default 64 was selected
// because it it was the smallest value on my mac where
// the banding wasn't obvious.
let noise_amount = g.noise.unwrap_or_else(|| {
if matches!(g.orientation, GradientOrientation::Radial { .. }) {
16
} else {
64
}
});
fn noise(rng: &fastrand::Rng, noise_amount: usize) -> f64 {
if noise_amount == 0 {
0.
} else {
rng.usize(0..noise_amount) as f64 * -1.
}
}
match g.orientation {
GradientOrientation::Horizontal => {
for (x, _, pixel) in imgbuf.enumerate_pixels_mut() {
*pixel = to_pixel(grad.at(remap(
x as f64 + noise(&rng, noise_amount),
0.0,
fw,
dmin,
dmax,
)));
}
}
GradientOrientation::Vertical => {
for (_, y, pixel) in imgbuf.enumerate_pixels_mut() {
*pixel = to_pixel(grad.at(remap(
y as f64 + noise(&rng, noise_amount),
0.0,
fh,
dmin,
dmax,
)));
}
}
GradientOrientation::Linear { angle } => {
let angle = angle.unwrap_or(0.0).to_radians();
for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
let (x, y) = (x as f64, y as f64);
let (x, y) = (x - fw / 2., y - fh / 2.);
let t = x * f64::cos(angle) - y * f64::sin(angle);
*pixel = to_pixel(grad.at(remap(
t + noise(&rng, noise_amount),
-fw / 2.,
fw / 2.,
dmin,
dmax,
)));
}
}
GradientOrientation::Radial { radius, cx, cy } => {
let radius = fw * radius.unwrap_or(0.5);
let cx = fw * cx.unwrap_or(0.5);
let cy = fh * cy.unwrap_or(0.5);
for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
let nx = noise(&rng, noise_amount);
let ny = noise(&rng, noise_amount);
let t = (nx + (x as f64 - cx).powi(2) + (ny + y as f64 - cy).powi(2))
.sqrt()
/ radius;
*pixel = to_pixel(grad.at(t));
}
}
}
let data = imgbuf.into_vec();
return Some(Arc::new(ImageData::with_data(
ImageDataType::new_single_frame(width, height, data),
)));
}
Err(err) => {
log::error!(
"window_background_gradient: error building gradient: {:#} {:?}",
err,
g
);
return None;
}
},
None => {}
}
match &config.window_background_image {
Some(p) => match std::fs::read(p) {
Ok(data) => {
log::info!("loaded {}", p.display());
let data = ImageDataType::EncodedFile(data).decode();
Some(Arc::new(ImageData::with_data(data)))
}
Err(err) => {
log::error!(
"Failed to load window_background_image {}: {}",
p.display(),
err
);
None
}
},
None => None,
}
}
fn reload_background_image(
config: &ConfigHandle,
image: &Option<Arc<ImageData>>,
dimensions: &Dimensions,
) -> Option<Arc<ImageData>> {
let data = load_background_image(config, dimensions)?;
match image {
Some(existing) if existing.hash() == data.data().compute_hash() => {
Some(Arc::clone(existing))
}
_ => Some(data),
}
}
impl TermWindow {
pub async fn new_window(mux_window_id: MuxWindowId) -> anyhow::Result<()> {
let config = configuration();

View File

@ -898,7 +898,7 @@ impl super::TermWindow {
));
let window_is_transparent =
self.window_background.is_some() || self.config.window_background_opacity != 1.0;
!self.window_background.is_empty() || self.config.window_background_opacity != 1.0;
let gl_state = self.render_state.as_ref().unwrap();
let white_space = gl_state.util_sprites.white_space.texture_coords();
let filled_box = gl_state.util_sprites.filled_box.texture_coords();
@ -1113,7 +1113,7 @@ impl super::TermWindow {
let filled_box = gl_state.util_sprites.filled_box.texture_coords();
let window_is_transparent =
self.window_background.is_some() || config.window_background_opacity != 1.0;
!self.window_background.is_empty() || config.window_background_opacity != 1.0;
let default_bg = palette
.resolve_bg(ColorAttribute::Default)
@ -1126,28 +1126,29 @@ impl super::TermWindow {
// Render the full window background
if pos.index == 0 {
match (self.window_background.as_ref(), self.allow_images) {
(Some(im), true) => {
// Render the window background image
let color = palette
.background
.to_linear()
.mul_alpha(config.window_background_opacity);
match (self.window_background.is_empty(), self.allow_images) {
(false, true) => {
// Render the window background image(s)
for layer in &self.window_background {
let color = palette.background.to_linear().mul_alpha(layer.def.opacity);
let (sprite, next_due) =
gl_state.glyph_cache.borrow_mut().cached_image(im, None)?;
self.update_next_frame_time(next_due);
let mut quad = layers[0].allocate()?;
quad.set_position(
self.dimensions.pixel_width as f32 / -2.,
self.dimensions.pixel_height as f32 / -2.,
self.dimensions.pixel_width as f32 / 2.,
self.dimensions.pixel_height as f32 / 2.,
);
quad.set_texture(sprite.texture_coords());
quad.set_is_background_image();
quad.set_hsv(config.window_background_image_hsb);
quad.set_fg_color(color);
let (sprite, next_due) = gl_state
.glyph_cache
.borrow_mut()
.cached_image(&layer.source, None)?;
self.update_next_frame_time(next_due);
let mut quad = layers[0].allocate()?;
quad.set_position(
self.dimensions.pixel_width as f32 / -2.,
self.dimensions.pixel_height as f32 / -2.,
self.dimensions.pixel_width as f32 / 2.,
self.dimensions.pixel_height as f32 / 2.,
);
quad.set_texture(sprite.texture_coords());
quad.set_is_background_image();
quad.set_hsv(Some(layer.def.hsb));
quad.set_fg_color(color);
}
}
_ if window_is_transparent && num_panes > 1 => {
// Avoid doubling up the background color: the panes
@ -1179,7 +1180,7 @@ impl super::TermWindow {
}
}
if num_panes > 1 && self.window_background.is_none() {
if num_panes > 1 && self.window_background.is_empty() {
// Per-pane, palette-specified background
let cell_width = self.render_metrics.cell_size.width as f32;
let cell_height = self.render_metrics.cell_size.height as f32;