mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 13:21:38 +03:00
WIP: fancier tab bar
`use_fancy_tab_bar` switches to an alternate rendering of the tab bar that uses the window_frame config to get a proportional title font to use to render tabs, as well as rendering a few additional elements to space out and make the tabs feel more like tabs. Computing the number of tabs doesn't respect the alternate font at this time. Formatted tab item foreground and background colors are also not respected at this time. refs: #1180
This commit is contained in:
parent
8652f2e7d3
commit
962e44bbfb
@ -307,7 +307,7 @@ fn default_inactive_titlebar_bg() -> RgbColor {
|
||||
}
|
||||
|
||||
fn default_active_titlebar_bg() -> RgbColor {
|
||||
RgbColor::new_8bpc(0x2b, 0x20, 0x42)
|
||||
RgbColor::new_8bpc(0x29, 0x29, 0x29)
|
||||
}
|
||||
|
||||
fn default_inactive_titlebar_fg() -> RgbColor {
|
||||
|
@ -978,6 +978,8 @@ pub struct Config {
|
||||
/// active tab. Clicking on a tab activates it.
|
||||
#[serde(default = "default_true")]
|
||||
pub enable_tab_bar: bool,
|
||||
#[serde(default)]
|
||||
pub use_fancy_tab_bar: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub tab_bar_at_bottom: bool,
|
||||
|
@ -71,6 +71,15 @@ impl Line {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_cells(cells: Vec<Cell>) -> Self {
|
||||
let bits = LineBits::NONE;
|
||||
Self {
|
||||
bits,
|
||||
cells,
|
||||
seqno: SEQ_ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_width(width: usize) -> Self {
|
||||
let mut cells = Vec::with_capacity(width);
|
||||
cells.resize_with(width, Cell::blank);
|
||||
@ -556,6 +565,17 @@ impl Line {
|
||||
self.update_last_change_seqno(seqno);
|
||||
}
|
||||
|
||||
pub fn remove_cell(&mut self, x: usize, seqno: SequenceNo) {
|
||||
if x >= self.cells.len() {
|
||||
// Already implicitly removed
|
||||
return;
|
||||
}
|
||||
self.invalidate_implicit_hyperlinks(seqno);
|
||||
self.invalidate_grapheme_at_or_before(x);
|
||||
self.cells.remove(x);
|
||||
self.update_last_change_seqno(seqno);
|
||||
}
|
||||
|
||||
pub fn erase_cell_with_margin(
|
||||
&mut self,
|
||||
x: usize,
|
||||
|
@ -24,7 +24,7 @@ use termwiz::color::RgbColor;
|
||||
use termwiz::image::{ImageData, ImageDataType};
|
||||
use termwiz::surface::CursorShape;
|
||||
use wezterm_font::units::*;
|
||||
use wezterm_font::{FontConfiguration, GlyphInfo};
|
||||
use wezterm_font::{FontConfiguration, GlyphInfo, LoadedFont};
|
||||
use wezterm_term::Underline;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@ -107,6 +107,7 @@ pub struct CachedGlyph<T: Texture2d> {
|
||||
pub brightness_adjust: f32,
|
||||
pub x_offset: PixelLength,
|
||||
pub y_offset: PixelLength,
|
||||
pub x_advance: PixelLength,
|
||||
pub bearing_x: PixelLength,
|
||||
pub bearing_y: PixelLength,
|
||||
pub texture: Option<Sprite<T>>,
|
||||
@ -313,6 +314,7 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
info: &GlyphInfo,
|
||||
style: &TextStyle,
|
||||
followed_by_space: bool,
|
||||
font: Option<&Rc<LoadedFont>>,
|
||||
) -> anyhow::Result<Rc<CachedGlyph<T>>> {
|
||||
let key = BorrowedGlyphKey {
|
||||
font_idx: info.font_idx,
|
||||
@ -327,7 +329,7 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
}
|
||||
metrics::histogram!("glyph_cache.glyph_cache.miss.rate", 1.);
|
||||
|
||||
let glyph = match self.load_glyph(info, style, followed_by_space) {
|
||||
let glyph = match self.load_glyph(info, style, font, followed_by_space) {
|
||||
Ok(g) => g,
|
||||
Err(err) => {
|
||||
if err
|
||||
@ -353,6 +355,7 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
brightness_adjust: 1.0,
|
||||
has_color: false,
|
||||
texture: None,
|
||||
x_advance: PixelLength::zero(),
|
||||
x_offset: PixelLength::zero(),
|
||||
y_offset: PixelLength::zero(),
|
||||
bearing_x: PixelLength::zero(),
|
||||
@ -371,6 +374,7 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
&mut self,
|
||||
info: &GlyphInfo,
|
||||
style: &TextStyle,
|
||||
font: Option<&Rc<LoadedFont>>,
|
||||
followed_by_space: bool,
|
||||
) -> anyhow::Result<Rc<CachedGlyph<T>>> {
|
||||
let base_metrics;
|
||||
@ -379,7 +383,10 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
let glyph;
|
||||
|
||||
{
|
||||
let font = self.fonts.resolve_font(style)?;
|
||||
let font = match font {
|
||||
Some(f) => Rc::clone(f),
|
||||
None => self.fonts.resolve_font(style)?,
|
||||
};
|
||||
base_metrics = font.metrics();
|
||||
glyph = font.rasterize_glyph(info.glyph_pos, info.font_idx)?;
|
||||
|
||||
@ -472,6 +479,7 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
texture: None,
|
||||
x_offset: info.x_offset * scale,
|
||||
y_offset: info.y_offset * scale,
|
||||
x_advance: info.x_advance * scale,
|
||||
bearing_x: PixelLength::zero(),
|
||||
bearing_y: PixelLength::zero(),
|
||||
scale,
|
||||
@ -488,6 +496,7 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
let bearing_y = glyph.bearing_y * scale;
|
||||
let x_offset = info.x_offset * scale;
|
||||
let y_offset = info.y_offset * scale;
|
||||
let x_advance = info.x_advance * scale;
|
||||
|
||||
let (scale, raw_im) = if scale != 1.0 {
|
||||
log::trace!(
|
||||
@ -513,6 +522,7 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
texture: Some(tex),
|
||||
x_offset,
|
||||
y_offset,
|
||||
x_advance,
|
||||
bearing_x,
|
||||
bearing_y,
|
||||
scale,
|
||||
|
@ -20,13 +20,14 @@ pub struct TabBarState {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TabBarItem {
|
||||
None,
|
||||
Tab(usize),
|
||||
Tab { tab_idx: usize, active: bool },
|
||||
NewTabButton,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct TabEntry {
|
||||
item: TabBarItem,
|
||||
pub struct TabEntry {
|
||||
pub item: TabBarItem,
|
||||
pub title: Line,
|
||||
x: usize,
|
||||
width: usize,
|
||||
}
|
||||
@ -70,11 +71,11 @@ fn call_format_tab_title(
|
||||
let items = <Vec<FormatItem>>::from_lua(v, &*lua)?;
|
||||
|
||||
let esc = format_as_escapes(items.clone())?;
|
||||
let cells = parse_status_text(&esc, CellAttributes::default());
|
||||
let line = parse_status_text(&esc, CellAttributes::default());
|
||||
|
||||
Ok(Some(TitleText {
|
||||
items,
|
||||
len: cells.len(),
|
||||
len: line.cells().len(),
|
||||
}))
|
||||
}
|
||||
_ => {
|
||||
@ -162,6 +163,10 @@ impl TabBarState {
|
||||
&self.line
|
||||
}
|
||||
|
||||
pub fn items(&self) -> &[TabEntry] {
|
||||
&self.items
|
||||
}
|
||||
|
||||
/// Build a new tab bar from the current state
|
||||
/// mouse_x is some if the mouse is on the same row as the tab bar.
|
||||
/// title_width is the total number of cell columns in the window.
|
||||
@ -217,7 +222,7 @@ impl TabBarState {
|
||||
let number_of_tabs = tab_titles.len();
|
||||
|
||||
let available_cells =
|
||||
title_width.saturating_sub(number_of_tabs.saturating_sub(1) + new_tab.len());
|
||||
title_width.saturating_sub(number_of_tabs.saturating_sub(1) + new_tab.cells().len());
|
||||
let tab_width_max = if available_cells >= titles_len {
|
||||
// We can render each title with its full width
|
||||
usize::max_value()
|
||||
@ -227,7 +232,7 @@ impl TabBarState {
|
||||
}
|
||||
.min(config.tab_max_width);
|
||||
|
||||
let mut line = Line::with_width(title_width);
|
||||
let mut line = Line::with_width(0);
|
||||
|
||||
let mut x = 0;
|
||||
let mut items = vec![];
|
||||
@ -259,44 +264,45 @@ impl TabBarState {
|
||||
let tab_start_idx = x;
|
||||
|
||||
let esc = format_as_escapes(tab_title.items.clone()).expect("already parsed ok above");
|
||||
let cells = parse_status_text(&esc, cell_attrs.clone());
|
||||
let mut n = 0;
|
||||
for cell in cells {
|
||||
let len = cell.width();
|
||||
if n + len > tab_width_max {
|
||||
break;
|
||||
}
|
||||
line.set_cell(x, cell, SEQ_ZERO);
|
||||
x += len;
|
||||
n += len;
|
||||
let mut tab_line = parse_status_text(&esc, cell_attrs.clone());
|
||||
|
||||
let title = tab_line.clone();
|
||||
if tab_line.cells().len() > tab_width_max {
|
||||
tab_line.resize(tab_width_max, SEQ_ZERO);
|
||||
}
|
||||
|
||||
let width = tab_line.cells().len();
|
||||
|
||||
items.push(TabEntry {
|
||||
item: TabBarItem::Tab(tab_idx),
|
||||
item: TabBarItem::Tab { tab_idx, active },
|
||||
title,
|
||||
x: tab_start_idx,
|
||||
width: x - tab_start_idx,
|
||||
width,
|
||||
});
|
||||
|
||||
line.append_line(tab_line, SEQ_ZERO);
|
||||
x += width;
|
||||
}
|
||||
|
||||
// New tab button
|
||||
{
|
||||
let hover = is_tab_hover(mouse_x, x, new_tab_hover.len());
|
||||
let hover = is_tab_hover(mouse_x, x, new_tab_hover.cells().len());
|
||||
|
||||
let cells = if hover { &new_tab_hover } else { &new_tab };
|
||||
let new_tab_button = if hover { &new_tab_hover } else { &new_tab };
|
||||
|
||||
let button_start = x;
|
||||
let width = new_tab_button.cells().len();
|
||||
|
||||
for c in cells {
|
||||
let len = c.width();
|
||||
line.set_cell(x, c.clone(), SEQ_ZERO);
|
||||
x += len;
|
||||
}
|
||||
line.append_line(new_tab_button.clone(), SEQ_ZERO);
|
||||
|
||||
items.push(TabEntry {
|
||||
item: TabBarItem::NewTabButton,
|
||||
title: new_tab_button.clone(),
|
||||
x: button_start,
|
||||
width: x - button_start,
|
||||
width,
|
||||
});
|
||||
|
||||
x += width;
|
||||
}
|
||||
|
||||
let black_cell = Cell::blank_with_attrs(
|
||||
@ -305,28 +311,28 @@ impl TabBarState {
|
||||
.clone(),
|
||||
);
|
||||
|
||||
for idx in x..title_width {
|
||||
line.set_cell(idx, black_cell.clone(), SEQ_ZERO);
|
||||
let status_space_available = title_width.saturating_sub(x);
|
||||
let mut status_line = parse_status_text(right_status, black_cell.attrs().clone());
|
||||
items.push(TabEntry {
|
||||
item: TabBarItem::None,
|
||||
title: status_line.clone(),
|
||||
x,
|
||||
width: status_space_available,
|
||||
});
|
||||
|
||||
while status_line.cells().len() > status_space_available {
|
||||
status_line.remove_cell(0, SEQ_ZERO);
|
||||
}
|
||||
|
||||
let rhs_cells = parse_status_text(right_status, black_cell.attrs().clone());
|
||||
let rhs_len = rhs_cells.len().min(title_width.saturating_sub(x));
|
||||
let skip = rhs_cells.len() - rhs_len;
|
||||
|
||||
for (idx, cell) in rhs_cells.into_iter().skip(skip).rev().enumerate() {
|
||||
line.set_cell(title_width - (1 + idx), cell, SEQ_ZERO);
|
||||
line.append_line(status_line, SEQ_ZERO);
|
||||
while line.cells().len() < title_width {
|
||||
line.insert_cell(x, black_cell.clone(), title_width, SEQ_ZERO);
|
||||
}
|
||||
|
||||
Self { line, items }
|
||||
}
|
||||
|
||||
pub fn compute_ui_items(
|
||||
&self,
|
||||
y: usize,
|
||||
cell_height: usize,
|
||||
cell_width: usize,
|
||||
width: usize,
|
||||
) -> Vec<UIItem> {
|
||||
pub fn compute_ui_items(&self, y: usize, cell_height: usize, cell_width: usize) -> Vec<UIItem> {
|
||||
let mut items = vec![];
|
||||
let mut last_x = 0;
|
||||
|
||||
@ -341,19 +347,11 @@ impl TabBarState {
|
||||
last_x += entry.width;
|
||||
}
|
||||
|
||||
items.push(UIItem {
|
||||
x: last_x * cell_width,
|
||||
width: width - (last_x * cell_width),
|
||||
y,
|
||||
height: cell_height,
|
||||
item_type: UIItemType::TabBar(TabBarItem::None),
|
||||
});
|
||||
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_status_text(text: &str, default_cell: CellAttributes) -> Vec<Cell> {
|
||||
fn parse_status_text(text: &str, default_cell: CellAttributes) -> Line {
|
||||
let mut pen = default_cell.clone();
|
||||
let mut cells = vec![];
|
||||
let mut ignoring = false;
|
||||
@ -444,5 +442,5 @@ fn parse_status_text(text: &str, default_cell: CellAttributes) -> Vec<Cell> {
|
||||
}
|
||||
});
|
||||
flush_print(&mut print_buffer, &mut cells, &pen);
|
||||
cells
|
||||
Line::from_cells(cells)
|
||||
}
|
||||
|
@ -125,6 +125,15 @@ pub struct UIItem {
|
||||
pub item_type: UIItemType,
|
||||
}
|
||||
|
||||
impl UIItem {
|
||||
pub fn hit_test(&self, x: isize, y: isize) -> bool {
|
||||
x >= self.x as isize
|
||||
&& x <= (self.x + self.width) as isize
|
||||
&& y >= self.y as isize
|
||||
&& y <= (self.y + self.height) as isize
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SemanticZoneCache {
|
||||
seqno: SequenceNo,
|
||||
@ -525,6 +534,15 @@ impl TermWindow {
|
||||
let render_metrics = RenderMetrics::new(&fontconfig)?;
|
||||
log::trace!("using render_metrics {:#?}", render_metrics);
|
||||
|
||||
// Initially we have only a single tab, so take that into account
|
||||
// for the tab bar state.
|
||||
let show_tab_bar = config.enable_tab_bar && !config.hide_tab_bar_if_only_one_tab;
|
||||
let tab_bar_height = if show_tab_bar {
|
||||
fontconfig.title_font()?.metrics().cell_height.get() as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let terminal_size = PtySize {
|
||||
rows: physical_rows as u16,
|
||||
cols: physical_cols as u16,
|
||||
@ -532,20 +550,15 @@ impl TermWindow {
|
||||
pixel_height: (render_metrics.cell_size.height as usize * physical_rows) as u16,
|
||||
};
|
||||
|
||||
// Initially we have only a single tab, so take that into account
|
||||
// for the tab bar state.
|
||||
let show_tab_bar = config.enable_tab_bar && !config.hide_tab_bar_if_only_one_tab;
|
||||
|
||||
let rows_with_tab_bar = if show_tab_bar { 1 } else { 0 } + terminal_size.rows;
|
||||
|
||||
let dimensions = Dimensions {
|
||||
pixel_width: (terminal_size.pixel_width
|
||||
+ config.window_padding.left
|
||||
+ resize::effective_right_padding(&config, &render_metrics))
|
||||
as usize,
|
||||
pixel_height: ((rows_with_tab_bar * render_metrics.cell_size.height as u16)
|
||||
pixel_height: ((terminal_size.rows * render_metrics.cell_size.height as u16)
|
||||
+ config.window_padding.top
|
||||
+ config.window_padding.bottom) as usize,
|
||||
+ config.window_padding.bottom) as usize
|
||||
+ tab_bar_height,
|
||||
dpi,
|
||||
};
|
||||
|
||||
|
@ -23,12 +23,7 @@ impl super::TermWindow {
|
||||
self.ui_items
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|item| {
|
||||
x >= item.x as isize
|
||||
&& x <= (item.x + item.width) as isize
|
||||
&& y >= item.y as isize
|
||||
&& y <= (item.y + item.height) as isize
|
||||
})
|
||||
.find(|item| item.hit_test(x, y))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
@ -64,37 +59,26 @@ 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 y = (event
|
||||
.coords
|
||||
.y
|
||||
.sub(config.window_padding.top as isize)
|
||||
.sub(first_line_offset)
|
||||
.max(0)
|
||||
/ self.render_metrics.cell_size.height) as i64;
|
||||
|
||||
let first_line_offset = if self.show_tab_bar && !self.config.tab_bar_at_bottom {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let tab_bar_y = if self.config.tab_bar_at_bottom {
|
||||
let num_rows = self
|
||||
.dimensions
|
||||
.pixel_height
|
||||
.sub((config.window_padding.top + config.window_padding.bottom) as usize)
|
||||
/ self.render_metrics.cell_size.height as usize;
|
||||
num_rows - 1
|
||||
} else {
|
||||
0
|
||||
} as i64;
|
||||
let in_tab_bar = self.show_tab_bar && y == tab_bar_y && event.coords.y >= 0;
|
||||
|
||||
let x = (event
|
||||
.coords
|
||||
.x
|
||||
.sub(config.window_padding.left as isize)
|
||||
.max(0) as f32)
|
||||
/ self.render_metrics.cell_size.width as f32;
|
||||
let x = if !in_tab_bar && !pane.is_mouse_grabbed() {
|
||||
let x = if !pane.is_mouse_grabbed() {
|
||||
// Round the x coordinate so that we're a bit more forgiving of
|
||||
// the horizontal position when selecting cells
|
||||
x.round()
|
||||
@ -105,9 +89,6 @@ impl super::TermWindow {
|
||||
|
||||
self.last_mouse_coords = (x, y);
|
||||
|
||||
// y position relative to top of viewport (not including tab bar)
|
||||
let term_y = y.saturating_sub(first_line_offset);
|
||||
|
||||
match event.kind {
|
||||
WMEK::Release(ref press) => {
|
||||
self.current_mouse_buttons.retain(|p| p != press);
|
||||
@ -169,7 +150,7 @@ impl super::TermWindow {
|
||||
}
|
||||
|
||||
if let Some((item, start_event)) = self.dragging.take() {
|
||||
self.drag_ui_item(item, start_event, x, term_y, event, context);
|
||||
self.drag_ui_item(item, start_event, x, y, event, context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -182,20 +163,23 @@ impl super::TermWindow {
|
||||
(Some(prior), Some(item)) => {
|
||||
self.leave_ui_item(&prior);
|
||||
self.enter_ui_item(item);
|
||||
context.invalidate();
|
||||
}
|
||||
(Some(prior), None) => {
|
||||
self.leave_ui_item(&prior);
|
||||
context.invalidate();
|
||||
}
|
||||
(None, Some(item)) => {
|
||||
self.enter_ui_item(item);
|
||||
context.invalidate();
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
|
||||
if let Some(item) = ui_item {
|
||||
self.mouse_event_ui_item(item, pane, term_y, event, context);
|
||||
self.mouse_event_ui_item(item, pane, y, event, context);
|
||||
} else {
|
||||
self.mouse_event_terminal(pane, x, term_y, event, context);
|
||||
self.mouse_event_terminal(pane, x, y, event, context);
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,10 +301,10 @@ impl super::TermWindow {
|
||||
) {
|
||||
match event.kind {
|
||||
WMEK::Press(MousePress::Left) => match item {
|
||||
TabBarItem::Tab(tab_idx) => {
|
||||
TabBarItem::Tab { tab_idx, .. } => {
|
||||
self.activate_tab(tab_idx as isize).ok();
|
||||
}
|
||||
TabBarItem::NewTabButton => {
|
||||
TabBarItem::NewTabButton { .. } => {
|
||||
self.spawn_tab(&SpawnTabDomain::CurrentPaneDomain);
|
||||
}
|
||||
TabBarItem::None => {
|
||||
@ -330,16 +314,16 @@ impl super::TermWindow {
|
||||
}
|
||||
},
|
||||
WMEK::Press(MousePress::Middle) => match item {
|
||||
TabBarItem::Tab(tab_idx) => {
|
||||
TabBarItem::Tab { tab_idx, .. } => {
|
||||
self.close_tab_idx(tab_idx).ok();
|
||||
}
|
||||
TabBarItem::NewTabButton | TabBarItem::None => {}
|
||||
TabBarItem::NewTabButton { .. } | TabBarItem::None => {}
|
||||
},
|
||||
WMEK::Press(MousePress::Right) => match item {
|
||||
TabBarItem::Tab(_) => {
|
||||
TabBarItem::Tab { .. } => {
|
||||
self.show_tab_navigator();
|
||||
}
|
||||
TabBarItem::NewTabButton => {
|
||||
TabBarItem::NewTabButton { .. } => {
|
||||
self.show_launcher();
|
||||
}
|
||||
TabBarItem::None => {}
|
||||
|
@ -2,6 +2,7 @@ use crate::customglyph::BlockKey;
|
||||
use crate::glium::texture::SrgbTexture2d;
|
||||
use crate::glyphcache::{CachedGlyph, GlyphCache};
|
||||
use crate::shapecache::*;
|
||||
use crate::tabbar::{TabBarItem, TabEntry};
|
||||
use crate::termwindow::{
|
||||
BorrowedShapeCacheKey, MappedQuads, RenderState, ScrollHit, ShapedInfo, TermWindowNotif,
|
||||
UIItem, UIItemType,
|
||||
@ -15,7 +16,7 @@ use ::window::glium::uniforms::{
|
||||
use ::window::glium::{uniform, BlendingFunction, LinearBlendingFactor, Surface};
|
||||
use ::window::WindowOps;
|
||||
use anyhow::anyhow;
|
||||
use config::{ConfigHandle, HsbTransform, TextStyle, VisualBellTarget};
|
||||
use config::{ConfigHandle, HsbTransform, TabBarColors, TextStyle, VisualBellTarget};
|
||||
use mux::pane::Pane;
|
||||
use mux::renderable::{RenderableDimensions, StableCursorPosition};
|
||||
use mux::tab::{PositionedPane, PositionedSplit, SplitDirection};
|
||||
@ -27,7 +28,7 @@ use termwiz::cell::{unicode_column_width, Blink};
|
||||
use termwiz::cellcluster::CellCluster;
|
||||
use termwiz::surface::{CursorShape, CursorVisibility};
|
||||
use wezterm_font::units::PixelLength;
|
||||
use wezterm_font::{ClearShapeCache, GlyphInfo};
|
||||
use wezterm_font::{ClearShapeCache, FontMetrics, GlyphInfo, LoadedFont};
|
||||
use wezterm_term::color::{ColorAttribute, ColorPalette, RgbColor};
|
||||
use wezterm_term::{CellAttributes, Line, StableRowIndex};
|
||||
use window::bitmaps::atlas::SpriteSlice;
|
||||
@ -35,6 +36,7 @@ use window::bitmaps::Texture2d;
|
||||
use window::color::LinearRgba;
|
||||
|
||||
pub struct RenderScreenLineOpenGLParams<'a> {
|
||||
pub first_line_offset: f32,
|
||||
pub line_idx: usize,
|
||||
pub stable_line_idx: Option<StableRowIndex>,
|
||||
pub line: &'a Line,
|
||||
@ -265,7 +267,242 @@ impl super::TermWindow {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint_one_tab(
|
||||
&self,
|
||||
mut pos_x: f32,
|
||||
item: &TabEntry,
|
||||
colors: &TabBarColors,
|
||||
style: &TextStyle,
|
||||
font: &Rc<LoadedFont>,
|
||||
metrics: &FontMetrics,
|
||||
layers: &mut [MappedQuads; 3],
|
||||
) -> anyhow::Result<(f32, UIItem)> {
|
||||
let left_offset = self.dimensions.pixel_width as f32 / 2.;
|
||||
let top_offset = self.dimensions.pixel_height as f32 / 2.;
|
||||
|
||||
let top_y = metrics.cell_height.get() as f32 / 4.;
|
||||
|
||||
let cell_clusters = item.title.cluster();
|
||||
|
||||
let pos_y = top_y + metrics.cell_height.get() as f32 + metrics.descender.get() as f32;
|
||||
|
||||
let gl_state = self.render_state.as_ref().unwrap();
|
||||
let mut shaped = vec![];
|
||||
let mut width = 0.;
|
||||
for cluster in &cell_clusters {
|
||||
let glyph_info =
|
||||
self.cached_cluster_shape(style, &cluster, &gl_state, &item.title, Some(font))?;
|
||||
for info in glyph_info.iter() {
|
||||
width += info.glyph.x_advance.get() as f32;
|
||||
}
|
||||
shaped.push(glyph_info);
|
||||
}
|
||||
|
||||
let hover_x_start = pos_x + metrics.cell_width.get() as f32 / 4.;
|
||||
let hover_x_end = hover_x_start + width; //+ metrics.cell_width.get() as f32 /2.;
|
||||
|
||||
let hover = match &self.current_mouse_event {
|
||||
Some(event) => {
|
||||
let mouse_x = event.coords.x as f32;
|
||||
let mouse_y = event.coords.y as f32;
|
||||
mouse_x as f32 >= hover_x_start
|
||||
&& mouse_x as f32 <= hover_x_end
|
||||
&& mouse_y as f32 >= top_y
|
||||
&& mouse_y as f32 <= metrics.cell_height.get() as f32 * 1.75
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
|
||||
let (bg_color, fg_color, is_status) = match item.item {
|
||||
TabBarItem::Tab { active, .. } => {
|
||||
let c = if active {
|
||||
&colors.active_tab
|
||||
} else if hover {
|
||||
&colors.inactive_tab_hover
|
||||
} else {
|
||||
&colors.inactive_tab
|
||||
};
|
||||
(c.bg_color, c.fg_color, false)
|
||||
}
|
||||
TabBarItem::NewTabButton => {
|
||||
let c = if hover {
|
||||
&colors.new_tab_hover
|
||||
} else {
|
||||
&colors.new_tab
|
||||
};
|
||||
(c.bg_color, c.fg_color, false)
|
||||
}
|
||||
TabBarItem::None => (colors.background, colors.inactive_tab.fg_color, true),
|
||||
};
|
||||
let glyph_color = rgbcolor_to_window_color(fg_color);
|
||||
|
||||
let bg_start;
|
||||
if is_status {
|
||||
bg_start = pos_x;
|
||||
// Right align status glyphs
|
||||
pos_x = 2. * left_offset - width;
|
||||
} else {
|
||||
pos_x += metrics.cell_width.get() as f32 / 4.0;
|
||||
bg_start = pos_x;
|
||||
// width += metrics.cell_width.get() as f32 / 2.0;
|
||||
}
|
||||
|
||||
{
|
||||
let mut quad = layers[0].allocate()?;
|
||||
quad.set_position(
|
||||
bg_start - left_offset,
|
||||
top_y - top_offset,
|
||||
bg_start + width - left_offset,
|
||||
top_y + (metrics.cell_height.get() as f32 * 1.5) - top_offset,
|
||||
);
|
||||
quad.set_texture_adjust(0., 0., 0., 0.);
|
||||
quad.set_texture(gl_state.util_sprites.filled_box.texture_coords());
|
||||
quad.set_is_background();
|
||||
quad.set_fg_color(rgbcolor_to_window_color(bg_color));
|
||||
quad.set_hsv(None);
|
||||
}
|
||||
|
||||
for glyph_info in shaped {
|
||||
for info in glyph_info.iter() {
|
||||
let glyph = &info.glyph;
|
||||
if let Some(texture) = glyph.texture.as_ref() {
|
||||
let x = pos_x + (glyph.x_offset.get() + glyph.bearing_x.get()) as f32;
|
||||
let y = top_y + pos_y - (glyph.y_offset.get() + glyph.bearing_y.get()) as f32;
|
||||
|
||||
let mut quad = layers[1].allocate()?;
|
||||
quad.set_position(
|
||||
x - left_offset,
|
||||
y - top_offset,
|
||||
(x - left_offset) + texture.coords.size.width as f32,
|
||||
(y - top_offset) + texture.coords.size.height as f32,
|
||||
);
|
||||
quad.set_fg_color(glyph_color);
|
||||
quad.set_texture(texture.texture_coords());
|
||||
quad.set_texture_adjust(0., 0., 0., 0.);
|
||||
quad.set_hsv(if glyph.brightness_adjust != 1.0 {
|
||||
let hsv = HsbTransform::default();
|
||||
Some(HsbTransform {
|
||||
brightness: hsv.brightness * glyph.brightness_adjust,
|
||||
..hsv
|
||||
})
|
||||
} else {
|
||||
None
|
||||
});
|
||||
quad.set_has_color(glyph.has_color);
|
||||
}
|
||||
pos_x += glyph.x_advance.get() as f32;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
bg_start + width,
|
||||
UIItem {
|
||||
x: if is_status { 0 } else { bg_start as usize },
|
||||
width: if is_status {
|
||||
self.dimensions.pixel_width
|
||||
} else {
|
||||
width as usize
|
||||
},
|
||||
y: if is_status { 0 } else { top_y as usize },
|
||||
height: (metrics.cell_height.get() as f32 * if is_status { 2. } else { 1.5 })
|
||||
as usize,
|
||||
item_type: UIItemType::TabBar(item.item.clone()),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn tab_bar_pixel_height(&self) -> anyhow::Result<f32> {
|
||||
if self.config.use_fancy_tab_bar {
|
||||
let font = self.fonts.title_font()?;
|
||||
Ok(font.metrics().cell_height.get() as f32 * 2.)
|
||||
} else {
|
||||
Ok(self.render_metrics.cell_size.height as f32)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_fancy_tab_bar(&self) -> anyhow::Result<Vec<UIItem>> {
|
||||
let colors = self
|
||||
.config
|
||||
.colors
|
||||
.as_ref()
|
||||
.and_then(|c| c.tab_bar.as_ref())
|
||||
.cloned()
|
||||
.unwrap_or_else(TabBarColors::default);
|
||||
let style = &self.config.window_frame.font;
|
||||
let font = self.fonts.title_font()?;
|
||||
let metrics = font.metrics();
|
||||
|
||||
let mut ui_items = vec![];
|
||||
|
||||
let items = self.tab_bar.items();
|
||||
|
||||
let gl_state = self.render_state.as_ref().unwrap();
|
||||
let vb = [&gl_state.vb[0], &gl_state.vb[1], &gl_state.vb[2]];
|
||||
let mut vb_mut0 = vb[0].current_vb_mut();
|
||||
let mut vb_mut1 = vb[1].current_vb_mut();
|
||||
let mut vb_mut2 = vb[2].current_vb_mut();
|
||||
let mut layers = [
|
||||
vb[0].map(&mut vb_mut0),
|
||||
vb[1].map(&mut vb_mut1),
|
||||
vb[2].map(&mut vb_mut2),
|
||||
];
|
||||
|
||||
// Overall window titlebar background
|
||||
{
|
||||
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.,
|
||||
(metrics.cell_height.get() as f32 * 2.) + self.dimensions.pixel_height as f32 / -2.,
|
||||
);
|
||||
quad.set_texture_adjust(0., 0., 0., 0.);
|
||||
quad.set_texture(gl_state.util_sprites.filled_box.texture_coords());
|
||||
quad.set_is_background();
|
||||
quad.set_fg_color(rgbcolor_to_window_color(if self.focused.is_some() {
|
||||
self.config.window_frame.active_titlebar_bg
|
||||
} else {
|
||||
self.config.window_frame.inactive_titlebar_bg
|
||||
}));
|
||||
quad.set_hsv(None);
|
||||
}
|
||||
// Dividing line that is logically part of the active tab
|
||||
{
|
||||
let mut quad = layers[0].allocate()?;
|
||||
quad.set_position(
|
||||
self.dimensions.pixel_width as f32 / -2.,
|
||||
(metrics.cell_height.get() as f32 * 1.75)
|
||||
+ self.dimensions.pixel_height as f32 / -2.,
|
||||
self.dimensions.pixel_width as f32 / 2.,
|
||||
(metrics.cell_height.get() as f32 * 2.) + self.dimensions.pixel_height as f32 / -2.,
|
||||
);
|
||||
quad.set_texture_adjust(0., 0., 0., 0.);
|
||||
quad.set_texture(gl_state.util_sprites.filled_box.texture_coords());
|
||||
quad.set_is_background();
|
||||
quad.set_fg_color(rgbcolor_to_window_color(colors.active_tab.bg_color));
|
||||
quad.set_hsv(None);
|
||||
}
|
||||
|
||||
let mut x = 0.;
|
||||
for item in items.iter() {
|
||||
let (new_x, item) =
|
||||
self.paint_one_tab(x, item, &colors, style, &font, &metrics, &mut layers)?;
|
||||
x = new_x;
|
||||
match item.item_type {
|
||||
UIItemType::TabBar(TabBarItem::None) => ui_items.insert(0, item),
|
||||
_ => ui_items.push(item),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ui_items)
|
||||
}
|
||||
|
||||
fn paint_tab_bar(&mut self) -> anyhow::Result<()> {
|
||||
if self.config.use_fancy_tab_bar {
|
||||
self.ui_items.append(&mut self.paint_fancy_tab_bar()?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let avail_height = self.dimensions.pixel_height.saturating_sub(
|
||||
(self.config.window_padding.top + self.config.window_padding.bottom) as usize,
|
||||
);
|
||||
@ -286,7 +523,6 @@ impl super::TermWindow {
|
||||
},
|
||||
self.render_metrics.cell_size.height as usize,
|
||||
self.render_metrics.cell_size.width as usize,
|
||||
self.dimensions.pixel_width,
|
||||
));
|
||||
|
||||
let window_is_transparent =
|
||||
@ -315,6 +551,7 @@ impl super::TermWindow {
|
||||
];
|
||||
self.render_screen_line_opengl(
|
||||
RenderScreenLineOpenGLParams {
|
||||
first_line_offset: 0.,
|
||||
line_idx: tab_bar_y,
|
||||
stable_line_idx: None,
|
||||
line: self.tab_bar.line(),
|
||||
@ -375,9 +612,9 @@ impl super::TermWindow {
|
||||
let palette = pos.pane.palette();
|
||||
|
||||
let first_line_offset = if self.show_tab_bar && !self.config.tab_bar_at_bottom {
|
||||
1
|
||||
self.tab_bar_pixel_height()?
|
||||
} else {
|
||||
0
|
||||
0.
|
||||
};
|
||||
|
||||
let cursor = pos.pane.get_cursor_position();
|
||||
@ -497,7 +734,8 @@ impl super::TermWindow {
|
||||
+ (pos.left as f32 * cell_width)
|
||||
+ self.config.window_padding.left as f32;
|
||||
let pos_y = (self.dimensions.pixel_height as f32 / -2.)
|
||||
+ ((first_line_offset + pos.top) as f32 * cell_height)
|
||||
+ first_line_offset
|
||||
+ (pos.top as f32 * cell_height)
|
||||
+ self.config.window_padding.top as f32;
|
||||
|
||||
quad.set_position(
|
||||
@ -565,7 +803,8 @@ impl super::TermWindow {
|
||||
+ (pos.left as f32 * cell_width)
|
||||
+ self.config.window_padding.left as f32;
|
||||
let pos_y = (self.dimensions.pixel_height as f32 / -2.)
|
||||
+ ((first_line_offset + pos.top) as f32 * cell_height)
|
||||
+ first_line_offset
|
||||
+ (pos.top as f32 * cell_height)
|
||||
+ self.config.window_padding.top as f32;
|
||||
|
||||
quad.set_position(
|
||||
@ -658,7 +897,8 @@ impl super::TermWindow {
|
||||
|
||||
self.render_screen_line_opengl(
|
||||
RenderScreenLineOpenGLParams {
|
||||
line_idx: line_idx + first_line_offset,
|
||||
first_line_offset,
|
||||
line_idx: line_idx,
|
||||
stable_line_idx: Some(stable_row),
|
||||
line: &line,
|
||||
selection: selrange,
|
||||
@ -809,9 +1049,9 @@ impl super::TermWindow {
|
||||
let cell_height = self.render_metrics.cell_size.height as f32;
|
||||
|
||||
let first_row_offset = if self.show_tab_bar && !self.config.tab_bar_at_bottom {
|
||||
1
|
||||
self.tab_bar_pixel_height()?
|
||||
} else {
|
||||
0
|
||||
0.
|
||||
};
|
||||
|
||||
let block = BlockKey::from_char(if split.direction == SplitDirection::Horizontal {
|
||||
@ -835,7 +1075,8 @@ impl super::TermWindow {
|
||||
quad.set_has_color(false);
|
||||
|
||||
let pos_y = (self.dimensions.pixel_height as f32 / -2.)
|
||||
+ (split.top + first_row_offset) as f32 * cell_height
|
||||
+ split.top as f32 * cell_height
|
||||
+ first_row_offset
|
||||
+ self.config.window_padding.top as f32;
|
||||
let pos_x = (self.dimensions.pixel_width as f32 / -2.)
|
||||
+ split.left as f32 * cell_width
|
||||
@ -852,7 +1093,8 @@ impl super::TermWindow {
|
||||
x: self.config.window_padding.left as usize + (split.left * cell_width as usize),
|
||||
width: cell_width as usize,
|
||||
y: self.config.window_padding.top as usize
|
||||
+ (split.top + first_row_offset) * cell_height as usize,
|
||||
+ first_row_offset as usize
|
||||
+ split.top * cell_height as usize,
|
||||
height: split.size * cell_height as usize,
|
||||
item_type: UIItemType::Split(split.clone()),
|
||||
});
|
||||
@ -867,7 +1109,8 @@ impl super::TermWindow {
|
||||
x: self.config.window_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
|
||||
+ (split.top + first_row_offset) * cell_height as usize,
|
||||
+ first_row_offset as usize
|
||||
+ split.top * cell_height as usize,
|
||||
height: cell_height as usize,
|
||||
item_type: UIItemType::Split(split.clone()),
|
||||
});
|
||||
@ -897,10 +1140,6 @@ impl super::TermWindow {
|
||||
self.paint_pane_opengl(&pos, num_panes)?;
|
||||
}
|
||||
|
||||
if self.show_tab_bar {
|
||||
self.paint_tab_bar()?;
|
||||
}
|
||||
|
||||
if let Some(pane) = self.get_active_pane_or_overlay() {
|
||||
let splits = self.get_splits();
|
||||
for split in &splits {
|
||||
@ -908,6 +1147,10 @@ impl super::TermWindow {
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_tab_bar {
|
||||
self.paint_tab_bar()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -937,6 +1180,7 @@ impl super::TermWindow {
|
||||
let cell_width = self.render_metrics.cell_size.width as f32;
|
||||
let cell_height = self.render_metrics.cell_size.height as f32;
|
||||
let pos_y = (self.dimensions.pixel_height as f32 / -2.)
|
||||
+ params.first_line_offset
|
||||
+ (params.line_idx + params.pos.map(|p| p.top).unwrap_or(0)) as f32 * cell_height
|
||||
+ self.config.window_padding.top as f32;
|
||||
|
||||
@ -1140,8 +1384,13 @@ impl super::TermWindow {
|
||||
let style_params = last_style.as_ref().expect("we literally just assigned it");
|
||||
|
||||
// Shape the printable text from this cluster
|
||||
let glyph_info =
|
||||
self.cached_cluster_shape(style_params.style, &cluster, &gl_state, params.line)?;
|
||||
let glyph_info = self.cached_cluster_shape(
|
||||
style_params.style,
|
||||
&cluster,
|
||||
&gl_state,
|
||||
params.line,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mut current_idx = cluster.first_cell_idx;
|
||||
|
||||
@ -1748,6 +1997,7 @@ impl super::TermWindow {
|
||||
style: &TextStyle,
|
||||
glyph_cache: &mut GlyphCache<SrgbTexture2d>,
|
||||
infos: &[GlyphInfo],
|
||||
font: Option<&Rc<LoadedFont>>,
|
||||
) -> anyhow::Result<Vec<Rc<CachedGlyph<SrgbTexture2d>>>> {
|
||||
let mut glyphs = Vec::with_capacity(infos.len());
|
||||
for info in infos {
|
||||
@ -1757,7 +2007,7 @@ impl super::TermWindow {
|
||||
None => false,
|
||||
};
|
||||
|
||||
glyphs.push(glyph_cache.cached_glyph(info, &style, followed_by_space)?);
|
||||
glyphs.push(glyph_cache.cached_glyph(info, &style, followed_by_space, font)?);
|
||||
}
|
||||
Ok(glyphs)
|
||||
}
|
||||
@ -1769,6 +2019,7 @@ impl super::TermWindow {
|
||||
cluster: &CellCluster,
|
||||
gl_state: &RenderState,
|
||||
line: &Line,
|
||||
font: Option<&Rc<LoadedFont>>,
|
||||
) -> anyhow::Result<Rc<Vec<ShapedInfo<SrgbTexture2d>>>> {
|
||||
let shape_resolve_start = Instant::now();
|
||||
let key = BorrowedShapeCacheKey {
|
||||
@ -1779,7 +2030,10 @@ impl super::TermWindow {
|
||||
Some(Ok(info)) => info,
|
||||
Some(Err(err)) => return Err(err),
|
||||
None => {
|
||||
let font = self.fonts.resolve_font(style)?;
|
||||
let font = match font {
|
||||
Some(f) => Rc::clone(f),
|
||||
None => self.fonts.resolve_font(style)?,
|
||||
};
|
||||
let window = self.window.as_ref().unwrap().clone();
|
||||
match font.shape(
|
||||
&cluster.text,
|
||||
@ -1794,6 +2048,7 @@ impl super::TermWindow {
|
||||
&style,
|
||||
&mut gl_state.glyph_cache.borrow_mut(),
|
||||
&info,
|
||||
Some(&font),
|
||||
)?;
|
||||
let shaped = Rc::new(ShapedInfo::process(
|
||||
&self.render_metrics,
|
||||
|
@ -119,6 +119,12 @@ impl super::TermWindow {
|
||||
|
||||
let config = &self.config;
|
||||
|
||||
let tab_bar_height = if self.show_tab_bar {
|
||||
self.tab_bar_pixel_height().unwrap_or(0.)
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
let (size, dims) = if let Some(cell_dims) = scale_changed_cells {
|
||||
// Scaling preserves existing terminal dimensions, yielding a new
|
||||
// overall set of window dimensions
|
||||
@ -129,11 +135,12 @@ impl super::TermWindow {
|
||||
pixel_width: cell_dims.cols as u16 * self.render_metrics.cell_size.width as u16,
|
||||
};
|
||||
|
||||
let rows = size.rows + if self.show_tab_bar { 1 } else { 0 };
|
||||
let rows = size.rows;
|
||||
let cols = size.cols;
|
||||
|
||||
let pixel_height = (rows * self.render_metrics.cell_size.height as u16)
|
||||
+ (config.window_padding.top + config.window_padding.bottom);
|
||||
+ (config.window_padding.top + config.window_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));
|
||||
@ -150,12 +157,12 @@ impl super::TermWindow {
|
||||
let avail_width = dimensions.pixel_width.saturating_sub(
|
||||
(config.window_padding.left + self.effective_right_padding(&config)) as usize,
|
||||
);
|
||||
let avail_height = dimensions.pixel_height.saturating_sub(
|
||||
(config.window_padding.top + config.window_padding.bottom) as usize,
|
||||
);
|
||||
let avail_height = dimensions
|
||||
.pixel_height
|
||||
.saturating_sub((config.window_padding.top + config.window_padding.bottom) as usize)
|
||||
.saturating_sub(tab_bar_height as usize);
|
||||
|
||||
let rows = (avail_height / self.render_metrics.cell_size.height as usize)
|
||||
.saturating_sub(if self.show_tab_bar { 1 } else { 0 });
|
||||
let rows = avail_height / self.render_metrics.cell_size.height as usize;
|
||||
let cols = avail_width / self.render_metrics.cell_size.width as usize;
|
||||
|
||||
let size = PtySize {
|
||||
@ -293,16 +300,21 @@ impl super::TermWindow {
|
||||
};
|
||||
|
||||
let show_tab_bar = config.enable_tab_bar && !config.hide_tab_bar_if_only_one_tab;
|
||||
let tab_bar_height = if show_tab_bar {
|
||||
self.tab_bar_pixel_height()? as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let rows_with_tab_bar = if show_tab_bar { 1 } else { 0 } + terminal_size.rows;
|
||||
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,
|
||||
pixel_height: ((rows_with_tab_bar * render_metrics.cell_size.height as u16)
|
||||
pixel_height: ((terminal_size.rows * render_metrics.cell_size.height as u16)
|
||||
+ config.window_padding.top
|
||||
+ config.window_padding.bottom) as usize,
|
||||
+ config.window_padding.bottom) as usize
|
||||
+ tab_bar_height,
|
||||
dpi: config.dpi.unwrap_or_else(|| ::window::default_dpi()) as usize,
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user