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

wezterm: add window background image and opacity options

I thought it would be cute to add an option to add a background image to
the window.

While playing around with the parameters, I accidentally implemented
window transparency on X11/Wayland, provided that you have a compositing
window manager.  I don't know of the transparency also works on macOS or
Windows as of yet; will try that out once I push this commit.

This commit introduces three new configuration options explained below.

In the future I'd like to allow specifying equivalent settings in a
color scheme, and then that would allow setting per-pane background
images.

```lua
return {
    --[[
     Specifies the path to a background image attachment file.
     The file can be any image format that the rust `image`
     crate is able to identify and load.
     A window background image is rendered into the background
     of the window before any other content.
     The image will be scaled to fit the window.
     If the path is not absolute, then it will taken as being
     relative to the directory containing wezterm.lua.
    ]]
    window_background_image = "/some/file",

    --[[ 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
     color of the cell in the current position.
     The default is 1.0 which is 100% opaque.  Setting it to a number
     between 0.0 and 1.0 will allow for the screen behind the window
     to "shine through" to varying degrees.
     This only works on systems with a compositing window manager.
     Setting opacity to a value other than 1.0 can impact render
     performance.
     ]]
    window_background_opacity = 1.0,

    --[[
    Specifies the alpha value to use when applying the default
    background color in a cell.  This is useful to apply a kind
    of "tint" to the background image if either window_background_image
    or window_background_opacity are in used.

    It can be a number between 0.0 and 1.0.
    The default is 0.0

    Larger numbers increase the amount of the color scheme's
    background color that is applied over the background image.

    This can be useful to increase effective contrast for text
    that is rendered over the top.
    ]]
    window_background_tint = 0.0,
}
```

refs: https://github.com/wez/wezterm/issues/141
This commit is contained in:
Wez Furlong 2020-10-18 09:47:55 -07:00
parent 2c0c89a97b
commit f2cbc182cd
6 changed files with 274 additions and 19 deletions

View File

@ -691,6 +691,45 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub window_padding: WindowPadding, pub window_padding: WindowPadding,
/// Specifies the path to a background image attachment file.
/// The file can be any image format that the rust `image`
/// crate is able to identify and load.
/// A window background image is rendered into the background
/// of the window before any other content.
///
/// The image will be scaled to fit the window.
#[serde(default)]
pub window_background_image: Option<PathBuf>,
/// 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
/// color of the cell in the current position.
/// The default is 1.0 which is 100% opaque. Setting it to a number
/// between 0.0 and 1.0 will allow for the screen behind the window
/// to "shine through" to varying degrees.
/// This only works on systems with a compositing window manager.
/// Setting opacity to a value other than 1.0 can impact render
/// performance.
#[serde(default = "default_one_point_oh")]
pub window_background_opacity: f32,
/// Specifies the alpha value to use when applying the default
/// background color in a cell. This is useful to apply a kind
/// of "tint" to the background image if either window_background_image
/// or window_background_opacity are in used.
///
/// It can be a number between 0.0 and 1.0.
/// The default is 0.0
///
/// Larger numbers increase the amount of the color scheme's
/// background color that is applied over the background image.
///
/// This can be useful to increase effective contrast for text
/// that is rendered over the top.
#[serde(default = "default_zero_point_oh")]
pub window_background_tint: f32,
/// Specifies how often a blinking cursor transitions between visible /// Specifies how often a blinking cursor transitions between visible
/// and invisible, expressed in milliseconds. /// and invisible, expressed in milliseconds.
/// Setting this to 0 disables blinking. /// Setting this to 0 disables blinking.
@ -757,6 +796,14 @@ pub struct Config {
pub enable_csi_u_key_encoding: bool, pub enable_csi_u_key_encoding: bool,
} }
fn default_zero_point_oh() -> f32 {
0.0
}
fn default_one_point_oh() -> f32 {
1.0
}
fn default_tab_max_width() -> usize { fn default_tab_max_width() -> usize {
16 16
} }
@ -946,6 +993,12 @@ impl Config {
*font_dir = dir; *font_dir = dir;
} }
} }
if let Some(path) = self.window_background_image.as_ref() {
if !path.is_absolute() {
cfg.window_background_image.replace(config_dir.join(path));
}
}
} }
if cfg.font_rules.is_empty() { if cfg.font_rules.is_empty() {

View File

@ -9,7 +9,9 @@ in vec2 o_cursor;
in vec4 o_cursor_color; in vec4 o_cursor_color;
uniform mat4 projection; uniform mat4 projection;
uniform bool window_bg_layer;
uniform bool bg_and_line_layer; uniform bool bg_and_line_layer;
uniform bool has_background_image;
uniform sampler2D glyph_tex; uniform sampler2D glyph_tex;
out vec4 color; out vec4 color;
@ -33,7 +35,23 @@ vec4 multiply(vec4 src, vec4 dst) {
} }
void main() { void main() {
if (bg_and_line_layer) { if (window_bg_layer) {
if (o_has_color == 2.0) {
// We're the window background image.
color = texture(glyph_tex, o_tex);
// Apply window_background_opacity to the background image
color.a = o_bg_color.a;
} else if (!has_background_image) {
// If there is no background image then take the cell background
color = o_bg_color;
} else {
// Nothing else should render on the background layer
// color = vec4(0,0,0,0);
discard;
}
} else if (bg_and_line_layer) {
// Note that o_bg_color is set to transparent if the background
// color is "default" and there is a window background attachment
color = o_bg_color; color = o_bg_color;
// Sample the underline glyph texture for this location. // Sample the underline glyph texture for this location.
@ -44,7 +62,7 @@ void main() {
// if the underline glyph isn't transparent in this position then // if the underline glyph isn't transparent in this position then
// we take the text fg color, otherwise we'll leave the color // we take the text fg color, otherwise we'll leave the color
// at the background color. // at the background color.
color.rgb = o_fg_color.rgb; color = o_fg_color;
} }
// Similar to the above: if the cursor texture isn't transparent // Similar to the above: if the cursor texture isn't transparent
@ -53,14 +71,20 @@ void main() {
// in the section above. // in the section above.
vec4 cursor_outline = texture(glyph_tex, o_cursor); vec4 cursor_outline = texture(glyph_tex, o_cursor);
if (cursor_outline.a != 0.0) { if (cursor_outline.a != 0.0) {
color.rgb = o_cursor_color.rgb; color = o_cursor_color;
} }
} else { } else {
color = texture(glyph_tex, o_tex); if (o_has_color == 2.0) {
if (o_has_color == 0.0) { // Don't render the background image on anything other than
// if it's not a color emoji, tint with the fg_color // the window_bg_layer.
color.rgb = o_fg_color.rgb; // color = vec4(0,0,0,0);
discard;
} else {
color = texture(glyph_tex, o_tex);
if (o_has_color == 0.0) {
// if it's not a color emoji, tint with the fg_color
color.rgb = o_fg_color.rgb;
}
} }
} }
} }

View File

@ -30,7 +30,16 @@ pub struct Vertex {
pub cursor_color: (f32, f32, f32, f32), pub cursor_color: (f32, f32, f32, f32),
pub bg_color: (f32, f32, f32, f32), pub bg_color: (f32, f32, f32, f32),
pub fg_color: (f32, f32, f32, f32), pub fg_color: (f32, f32, f32, f32),
// We use a float for this because I can't get
// bool or integer values to work:
// "bool can't be an in in the vertex shader" // "bool can't be an in in the vertex shader"
//
// has_color is effectively an enum with three
// possible values:
// 0.0 -> a regular monochrome text glyph
// 1.0 -> a color emoji glyph
// 2.0 -> a full color texture attached as the
// background image of the window
pub has_color: f32, pub has_color: f32,
} }
::window::glium::implement_vertex!( ::window::glium::implement_vertex!(
@ -56,6 +65,7 @@ pub struct Quads {
pub row_starts: Vec<usize>, pub row_starts: Vec<usize>,
/// The vertex index for the first vertex of the scroll bar thumb /// The vertex index for the first vertex of the scroll bar thumb
pub scroll_thumb: usize, pub scroll_thumb: usize,
pub background_image: usize,
} }
pub struct MappedQuads<'a> { pub struct MappedQuads<'a> {
@ -87,6 +97,13 @@ impl<'a> MappedQuads<'a> {
vert: &mut self.mapping[start..start + VERTICES_PER_CELL], vert: &mut self.mapping[start..start + VERTICES_PER_CELL],
} }
} }
pub fn background_image<'b>(&'b mut self) -> Quad<'b> {
let start = self.quads.background_image;
Quad {
vert: &mut self.mapping[start..start + VERTICES_PER_CELL],
}
}
} }
impl Quads { impl Quads {
@ -129,6 +146,14 @@ impl<'a> Quad<'a> {
} }
} }
/// Mark this quad as a background image.
/// Mutually exclusive with set_has_color.
pub fn set_is_background_image(&mut self) {
for v in self.vert.iter_mut() {
v.has_color = 2.0;
}
}
pub fn set_fg_color(&mut self, color: Color) { pub fn set_fg_color(&mut self, color: Color) {
let color = color.to_tuple_rgba(); let color = color.to_tuple_rgba();
for v in self.vert.iter_mut() { for v in self.vert.iter_mut() {

View File

@ -201,6 +201,10 @@ impl OpenGLRenderState {
idx idx
}; };
// Background image fills the entire window background
quads.background_image =
define_quad(width / -2.0, height / -2.0, width / 2.0, height / 2.0) as usize;
for y in 0..num_rows { for y in 0..num_rows {
let y_pos = (height / -2.0) + (y as f32 * cell_height) + padding_top; let y_pos = (height / -2.0) + (y as f32 * cell_height) + padding_top;

View File

@ -48,8 +48,9 @@ use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use termwiz::color::RgbColor; use termwiz::color::{ColorAttribute, RgbColor};
use termwiz::hyperlink::Hyperlink; use termwiz::hyperlink::Hyperlink;
use termwiz::image::ImageData;
use termwiz::surface::{CursorShape, CursorVisibility}; use termwiz::surface::{CursorShape, CursorVisibility};
use wezterm_term::color::ColorPalette; use wezterm_term::color::ColorPalette;
use wezterm_term::input::LastMouseClick; use wezterm_term::input::LastMouseClick;
@ -273,6 +274,8 @@ pub struct TermWindow {
tab_state: RefCell<HashMap<TabId, TabState>>, tab_state: RefCell<HashMap<TabId, TabState>>,
pane_state: RefCell<HashMap<PaneId, PaneState>>, pane_state: RefCell<HashMap<PaneId, PaneState>>,
window_background: Option<Arc<ImageData>>,
/// Gross workaround for managing async keyboard fetching /// Gross workaround for managing async keyboard fetching
/// just for middle mouse button paste function /// just for middle mouse button paste function
clipboard_contents: Arc<Mutex<Option<String>>>, clipboard_contents: Arc<Mutex<Option<String>>>,
@ -706,6 +709,7 @@ impl WindowCallbacks for TermWindow {
let guts = Box::new(Self { let guts = Box::new(Self {
window: None, window: None,
window_background: self.window_background.clone(),
focused: None, focused: None,
mux_window_id, mux_window_id,
fonts: Rc::clone(&self.fonts), fonts: Rc::clone(&self.fonts),
@ -874,9 +878,59 @@ pub fn effective_right_padding(config: &ConfigHandle, render_metrics: &RenderMet
} }
} }
fn load_background_image(config: &ConfigHandle) -> Option<Arc<ImageData>> {
match &config.window_background_image {
Some(p) => match std::fs::read(p) {
Ok(data) => {
log::error!("loaded {}", p.display());
Some(Arc::new(ImageData::with_raw_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>>,
) -> Option<Arc<ImageData>> {
match &config.window_background_image {
Some(p) => match std::fs::read(p) {
Ok(data) => {
if let Some(existing) = image {
if existing.data() == data {
return Some(Arc::clone(existing));
}
}
Some(Arc::new(ImageData::with_raw_data(data)))
}
Err(err) => {
log::error!(
"Failed to load window_background_image {}: {}",
p.display(),
err
);
None
}
},
None => None,
}
}
impl TermWindow { impl TermWindow {
pub fn new_window(mux_window_id: MuxWindowId) -> anyhow::Result<()> { pub fn new_window(mux_window_id: MuxWindowId) -> anyhow::Result<()> {
let config = configuration(); let config = configuration();
let window_background = load_background_image(&config);
let fontconfig = Rc::new(FontConfiguration::new()); let fontconfig = Rc::new(FontConfiguration::new());
let mux = Mux::get().unwrap(); let mux = Mux::get().unwrap();
let tab = mux.get_active_tab_for_window(mux_window_id).unwrap(); let tab = mux.get_active_tab_for_window(mux_window_id).unwrap();
@ -933,6 +987,7 @@ impl TermWindow {
dimensions.pixel_height, dimensions.pixel_height,
Box::new(Self { Box::new(Self {
window: None, window: None,
window_background,
focused: None, focused: None,
mux_window_id, mux_window_id,
fonts: fontconfig, fonts: fontconfig,
@ -1249,6 +1304,8 @@ impl TermWindow {
::window::os::windows::use_dead_keys(config.use_dead_keys); ::window::os::windows::use_dead_keys(config.use_dead_keys);
} }
self.window_background = reload_background_image(&config, &self.window_background);
let mux = Mux::get().unwrap(); let mux = Mux::get().unwrap();
let window = match mux.get_window(self.mux_window_id) { let window = match mux.get_window(self.mux_window_id) {
Some(window) => window, Some(window) => window,
@ -2569,7 +2626,8 @@ impl TermWindow {
let cursor_border_color = rgbcolor_to_window_color(palette.cursor_border); let cursor_border_color = rgbcolor_to_window_color(palette.cursor_border);
let foreground = rgbcolor_to_window_color(palette.foreground); let foreground = rgbcolor_to_window_color(palette.foreground);
let background = rgbcolor_to_window_color(palette.background); let background_alpha = (config.window_background_opacity * 255.0) as u8;
let background = rgbcolor_alpha_to_window_color(palette.background, background_alpha);
if self.show_tab_bar && pos.index == 0 { if self.show_tab_bar && pos.index == 0 {
let tab_dims = RenderableDimensions { let tab_dims = RenderableDimensions {
@ -2637,6 +2695,29 @@ impl TermWindow {
quad.set_cursor_color(rgbcolor_to_window_color(background_color)); quad.set_cursor_color(rgbcolor_to_window_color(background_color));
} }
{
let mut quad = quads.background_image();
let white_space = gl_state.util_sprites.white_space.texture_coords();
quad.set_underline(white_space);
quad.set_cursor(white_space);
quad.set_has_color(false);
let color = Color::rgba(0, 0, 0, background_alpha);
quad.set_texture_adjust(0., 0., 0., 0.);
quad.set_texture(white_space);
if let Some(im) = self.window_background.as_ref() {
if let Ok(sprite) = gl_state.glyph_cache.borrow_mut().cached_image(im) {
quad.set_texture(sprite.texture_coords());
quad.set_is_background_image();
}
}
quad.set_cursor_color(color);
quad.set_fg_color(color);
quad.set_bg_color(background);
quad.set_cursor_color(color);
}
let selrange = self.selection(pos.pane.pane_id()).range.clone(); let selrange = self.selection(pos.pane.pane_id()).range.clone();
for (line_idx, line) in lines.iter().enumerate() { for (line_idx, line) in lines.iter().enumerate() {
@ -2696,7 +2777,9 @@ impl TermWindow {
.magnify_filter(MagnifySamplerFilter::Nearest) .magnify_filter(MagnifySamplerFilter::Nearest)
.minify_filter(MinifySamplerFilter::Nearest); .minify_filter(MinifySamplerFilter::Nearest);
// Pass 1: Draw backgrounds, strikethrough and underline let has_background_image = self.window_background.is_some();
// Pass 1: Draw backgrounds
frame.draw( frame.draw(
&*vb, &*vb,
&gl_state.glyph_index_buffer, &gl_state.glyph_index_buffer,
@ -2704,7 +2787,9 @@ impl TermWindow {
&uniform! { &uniform! {
projection: projection, projection: projection,
glyph_tex: glyph_tex, glyph_tex: glyph_tex,
bg_and_line_layer: true, window_bg_layer: true,
bg_and_line_layer: false,
has_background_image: has_background_image,
}, },
&draw_params, &draw_params,
)?; )?;
@ -2731,7 +2816,7 @@ impl TermWindow {
..Default::default() ..Default::default()
}; };
// Pass 2: Draw glyphs // Pass 2: strikethrough and underline
frame.draw( frame.draw(
&*vb, &*vb,
&gl_state.glyph_index_buffer, &gl_state.glyph_index_buffer,
@ -2739,7 +2824,24 @@ impl TermWindow {
&uniform! { &uniform! {
projection: projection, projection: projection,
glyph_tex: glyph_tex, glyph_tex: glyph_tex,
window_bg_layer: false,
bg_and_line_layer: true,
has_background_image: has_background_image,
},
&draw_params,
)?;
// Pass 3: Draw glyphs
frame.draw(
&*vb,
&gl_state.glyph_index_buffer,
&gl_state.program,
&uniform! {
projection: projection,
glyph_tex: glyph_tex,
window_bg_layer: false,
bg_and_line_layer: false, bg_and_line_layer: false,
has_background_image: has_background_image,
}, },
&draw_params, &draw_params,
)?; )?;
@ -2782,6 +2884,7 @@ impl TermWindow {
}; };
let style = self.fonts.match_style(params.config, attrs); let style = self.fonts.match_style(params.config, attrs);
let bg_is_default = attrs.background == ColorAttribute::Default;
let bg_color = params.palette.resolve_bg(attrs.background); let bg_color = params.palette.resolve_bg(attrs.background);
let fg_color = match attrs.foreground { let fg_color = match attrs.foreground {
wezterm_term::color::ColorAttribute::Default => { wezterm_term::color::ColorAttribute::Default => {
@ -2809,19 +2912,29 @@ impl TermWindow {
_ => params.palette.resolve_fg(attrs.foreground), _ => params.palette.resolve_fg(attrs.foreground),
}; };
let (fg_color, bg_color) = { let (fg_color, bg_color, bg_is_default) = {
let mut fg = fg_color; let mut fg = fg_color;
let mut bg = bg_color; let mut bg = bg_color;
let mut bg_is_default = bg_is_default;
if attrs.reverse() { if attrs.reverse() {
std::mem::swap(&mut fg, &mut bg); std::mem::swap(&mut fg, &mut bg);
// reversed, so don't let it be transparent
bg_is_default = false;
} }
(fg, bg) (fg, bg, bg_is_default)
}; };
let glyph_color = rgbcolor_to_window_color(fg_color); let glyph_color = rgbcolor_to_window_color(fg_color);
let bg_color = rgbcolor_to_window_color(bg_color); let bg_color = if bg_is_default {
rgbcolor_alpha_to_window_color(
bg_color,
(params.config.window_background_tint * 255.0) as u8,
)
} else {
rgbcolor_to_window_color(bg_color)
};
// Shape the printable text from this cluster // Shape the printable text from this cluster
let glyph_info = { let glyph_info = {
@ -3010,13 +3123,18 @@ impl TermWindow {
// Even though we don't have a cell for these, they still // Even though we don't have a cell for these, they still
// hold the cursor or the selection so we need to compute // hold the cursor or the selection so we need to compute
// the colors in the usual way. // the colors in the usual way.
let (glyph_color, bg_color, cursor_shape) = self.compute_cell_fg_bg( let (glyph_color, bg_color, cursor_shape) = self.compute_cell_fg_bg(
params.stable_line_idx, params.stable_line_idx,
cell_idx, cell_idx,
params.cursor, params.cursor,
&params.selection, &params.selection,
params.foreground, params.foreground,
params.background, if self.window_background.is_some() {
Color::rgba(0, 0, 0, 0)
} else {
params.background
},
params.palette, params.palette,
params.pos.is_active, params.pos.is_active,
); );
@ -4029,7 +4147,11 @@ impl TermWindow {
} }
fn rgbcolor_to_window_color(color: RgbColor) -> Color { fn rgbcolor_to_window_color(color: RgbColor) -> Color {
Color::rgba(color.red, color.green, color.blue, 0xff) rgbcolor_alpha_to_window_color(color, 0xff)
}
fn rgbcolor_alpha_to_window_color(color: RgbColor, alpha: u8) -> Color {
Color::rgba(color.red, color.green, color.blue, alpha)
} }
fn window_mods_to_termwiz_mods(modifiers: ::window::Modifiers) -> termwiz::input::Modifiers { fn window_mods_to_termwiz_mods(modifiers: ::window::Modifiers) -> termwiz::input::Modifiers {

View File

@ -10,7 +10,9 @@ in vec2 cursor;
in vec4 cursor_color; in vec4 cursor_color;
uniform mat4 projection; uniform mat4 projection;
uniform bool window_bg_layer;
uniform bool bg_and_line_layer; uniform bool bg_and_line_layer;
uniform bool has_background_image;
out vec2 o_tex; out vec2 o_tex;
out vec4 o_fg_color; out vec4 o_fg_color;
@ -20,6 +22,14 @@ out vec2 o_underline;
out vec2 o_cursor; out vec2 o_cursor;
out vec4 o_cursor_color; out vec4 o_cursor_color;
// Returns a position that is outside of the viewport,
// such that this vertex effectively won't contribute
// the scene being rendered.
// There may be a better way to do this.
vec4 off_screen() {
return vec4(100.0, 100.0, 100.0, 100.0);
}
void main() { void main() {
o_tex = tex; o_tex = tex;
o_has_color = has_color; o_has_color = has_color;
@ -29,7 +39,24 @@ void main() {
o_cursor = cursor; o_cursor = cursor;
o_cursor_color = cursor_color; o_cursor_color = cursor_color;
if (bg_and_line_layer) { if (window_bg_layer) {
if (o_has_color == 2.0) {
// Background image takes up its full coordinates
gl_Position = projection * vec4(position, 0.0, 1.0);
} else if (!has_background_image) {
// If there is no background attachment, then we're
// rendering the cell background color and need to
// fill the cell with that color
gl_Position = projection * vec4(position, 0.0, 1.0);
} else {
// Nothing else should render on the background layer
gl_Position = off_screen();
}
} else if (o_has_color == 2.0) {
// If we're the background image and we're not rendering
// the background layer, then move this off screen
gl_Position = off_screen();
} else if (bg_and_line_layer) {
// Want to fill the whole cell when painting backgrounds // Want to fill the whole cell when painting backgrounds
gl_Position = projection * vec4(position, 0.0, 1.0); gl_Position = projection * vec4(position, 0.0, 1.0);
} else { } else {