feat: refine sidebar type and SSH editor

This commit is contained in:
kuddev committed 2026-07-15 23:41:59 +08:00
1 parent 67de759252
commit 992cc4f375
3 files changed
+216 -93

No files matched your search

+29 -7
View File
@@ -1236,13 +1236,15 @@ pub(super) fn draw_chrome(d: &mut Display) {
// inside the header band to keep clearance from the join.
let (pnl_x, pnl_y, _, _) = tab_layout.panel;
let tabs_chevron = if d.nebula_tabs_section_open { "\u{eab4}" } else { "\u{eab6}" };
const SECTION_TITLE_SCALE: f32 = design_tokens::type_scale::SECTION_CAPTION;
const SECTION_TITLE_SCALE: f32 = 0.82;
let section_title_tracking = s(0.65);
let section_title_flags = nebula_terminal::term::cell::Flags::BOLD;
d.renderer.draw_doc_text(
d.renderer.draw_doc_text_tracked(
&size,
pnl_x + s(16.0),
pnl_y + s(22.0),
SECTION_TITLE_SCALE,
section_title_tracking,
TXT_DIM,
section_title_flags,
&format!("TABS {tabs_chevron}"),
@@ -1293,7 +1295,9 @@ pub(super) fn draw_chrome(d: &mut Display) {
} else {
d.nebula_tab_labels.get(index).map(String::as_str).unwrap_or(".")
};
let max_chars = ((tab_w - reserved).max(cell_w) / cell_w).floor() as usize;
let row_tracking = s(0.35);
let max_chars =
((tab_w - reserved).max(cell_w) / (cell_w + row_tracking)).floor() as usize;
let label = truncate_tab_label(label, max_chars.max(1));
// Input box + selection/caret when renaming this tab. These
@@ -1304,6 +1308,7 @@ pub(super) fn draw_chrome(d: &mut Display) {
// why the rename box was invisible. Draw them now, before the
// label glyphs, so box/selection sit under the text.
let renaming_this = d.nebula_tab_rename.as_ref().is_some_and(|(i, _)| *i == index);
let label_tracking = if renaming_this { 0.0 } else { row_tracking };
let select_all = renaming_this && d.nebula_tab_rename_select_all;
if renaming_this {
let input_pad = s(4.0);
@@ -1400,10 +1405,12 @@ pub(super) fn draw_chrome(d: &mut Display) {
d.window.set_ime_cursor_area_px(caret_px, row_y, cell_w, tab_h);
}
d.renderer.draw_chrome_text(
d.renderer.draw_doc_text_tracked(
&size,
text_x,
cy,
1.0,
label_tracking,
if renaming_this {
if select_all {
Rgb::new(255, 255, 255) // White on the blue selection
@@ -1413,6 +1420,7 @@ pub(super) fn draw_chrome(d: &mut Display) {
} else {
color
},
nebula_terminal::term::cell::Flags::empty(),
&label,
&mut d.glyph_cache,
);
@@ -1447,11 +1455,12 @@ pub(super) fn draw_chrome(d: &mut Display) {
if tab_layout.hosts_header.2 > 0.0 {
let (hh_x, hh_y, _, hh_h) = tab_layout.hosts_header;
let hosts_chevron = if d.nebula_hosts_section_open { "\u{eab4}" } else { "\u{eab6}" };
d.renderer.draw_doc_text(
d.renderer.draw_doc_text_tracked(
&size,
hh_x + s(16.0),
hh_y + (hh_h - cell_h * SECTION_TITLE_SCALE) / 2.0,
SECTION_TITLE_SCALE,
section_title_tracking,
TXT_DIM,
section_title_flags,
&format!("SSH HOSTS {hosts_chevron}"),
@@ -1531,14 +1540,27 @@ pub(super) fn draw_chrome(d: &mut Display) {
// aliases run past the hover pill.
let text_x = hx + s(14.0);
let right = hx + hw - s(42.0);
let max_cols = (((right - text_x) / cell_w).floor() as usize).saturating_sub(2);
let row_tracking = s(0.35);
let max_cols = (((right - text_x) / (cell_w + row_tracking)).floor() as usize)
.saturating_sub(2);
let label = truncate_tab_label(name, max_cols.max(1));
d.renderer.draw_chrome_text(
&size,
text_x,
cy,
color,
&format!("\u{f489} {label}"),
"\u{f489}",
&mut d.glyph_cache,
);
d.renderer.draw_doc_text_tracked(
&size,
text_x + cell_w * 2.0,
cy,
1.0,
row_tracking,
color,
nebula_terminal::term::cell::Flags::empty(),
&label,
&mut d.glyph_cache,
);
if pinned {
+158 -68
View File
@@ -35,27 +35,28 @@ impl Display {
};
let (show_password, show_keys) = auth_sections(editor.auth);
let box_w = s(700.0).min(size.width() - s(32.0));
let box_w = s(520.0).min(size.width() - s(32.0));
let desired_h = if show_password && show_keys {
610.0
540.0
} else if show_keys {
520.0
460.0
} else if show_password {
440.0
400.0
} else {
370.0
330.0
};
let box_h = s(desired_h).min(size.height() - s(32.0));
let bx = (size.width() - box_w) * 0.5;
let resting_y = (size.height() - box_h) * 0.5;
let by = resting_y - (1.0 - progress) * s(14.0);
let pad = s(28.0);
let field_h = s(42.0);
let pad = s(24.0);
let field_h = s(40.0);
let field_w = box_w - pad * 2.0;
let destination = (bx + pad, by + s(76.0), field_w, field_h);
let auth_y = destination.1 + destination.3 + s(42.0);
let auth_gap = s(6.0);
let auth_w = (field_w - auth_gap * 4.0) / 5.0;
let auth_y = destination.1 + destination.3 + s(40.0);
let auth_track = (destination.0, auth_y, field_w, s(40.0));
let auth_pad = s(3.0);
let auth_w = (field_w - auth_pad * 2.0) / 5.0;
let auth_modes = [
SshAuthMode::Auto,
SshAuthMode::Password,
@@ -66,10 +67,15 @@ impl Display {
let auth = std::array::from_fn(|index| {
(
auth_modes[index],
(destination.0 + index as f32 * (auth_w + auth_gap), auth_y, auth_w, s(34.0)),
(
auth_track.0 + auth_pad + index as f32 * auth_w,
auth_track.1 + auth_pad,
auth_w,
auth_track.3 - auth_pad * 2.0,
),
)
});
let content_y = auth_y + s(76.0);
let content_y = auth_y + s(72.0);
let zero = (0.0, 0.0, 0.0, 0.0);
let password =
if show_password { (destination.0, content_y, field_w, field_h) } else { zero };
@@ -95,16 +101,17 @@ impl Display {
zero
};
let key_header_y = if show_password { save_toggle.1 + s(56.0) } else { content_y };
let key_header_y = if show_password { save_toggle.1 + s(54.0) } else { content_y };
let add_private_key = if show_keys {
(destination.0 + field_w - s(150.0), key_header_y - s(8.0), s(150.0), s(32.0))
(destination.0 + field_w - s(126.0), key_header_y - s(8.0), s(126.0), s(32.0))
} else {
zero
};
let key_rows_y = key_header_y + s(30.0);
let footer_y = by + box_h - s(58.0);
let footer_top = footer_y - s(16.0);
let available_rows =
(((footer_y - s(22.0) - key_rows_y) / s(36.0)).floor() as isize).max(1) as usize;
(((footer_top - s(16.0) - key_rows_y) / s(36.0)).floor() as isize).max(1) as usize;
let visible_start = editor.private_keys.len().saturating_sub(available_rows);
let visible_keys = if show_keys {
editor
@@ -128,13 +135,37 @@ impl Display {
Vec::new()
};
let primary_label = "保存 Enter";
let cancel_label = "取消 Esc";
let button_pad = s(16.0);
let primary_w = s(112.0).max(text_width(primary_label) + button_pad * 2.0);
let cancel_w = s(112.0).max(text_width(cancel_label) + button_pad * 2.0);
let primary_action = "保存";
let primary_key = "Enter";
let cancel_action = "取消";
let cancel_key = "Esc";
let key_pad = s(6.0);
let label_gap = s(8.0);
let button_pad = s(14.0);
let primary_key_w = text_width(primary_key) + key_pad * 2.0;
let cancel_key_w = text_width(cancel_key) + key_pad * 2.0;
let primary_w =
s(108.0).max(text_width(primary_action) + label_gap + primary_key_w + button_pad * 2.0);
let cancel_w =
s(100.0).max(text_width(cancel_action) + label_gap + cancel_key_w + button_pad * 2.0);
let primary = (bx + box_w - pad - primary_w, footer_y, primary_w, s(36.0));
let cancel = (primary.0 - s(12.0) - cancel_w, primary.1, cancel_w, s(36.0));
let primary_group_w = text_width(primary_action) + label_gap + primary_key_w;
let primary_group_x = primary.0 + (primary.2 - primary_group_w) * 0.5;
let primary_key_rect = (
primary_group_x + text_width(primary_action) + label_gap,
primary.1 + s(7.0),
primary_key_w,
primary.3 - s(14.0),
);
let cancel_group_w = text_width(cancel_action) + label_gap + cancel_key_w;
let cancel_group_x = cancel.0 + (cancel.2 - cancel_group_w) * 0.5;
let cancel_key_rect = (
cancel_group_x + text_width(cancel_action) + label_gap,
cancel.1 + s(7.0),
cancel_key_w,
cancel.3 - s(14.0),
);
self.nebula_ssh_editor_rects = Some(SshEditorRects {
destination,
@@ -170,6 +201,8 @@ impl Display {
skin.hairline,
),
UiQuad::solid(bx, by, box_w, box_h, s(12.0), skin.panel),
UiQuad::solid(bx, footer_top, box_w, box_h - (footer_top - by), 0.0, skin.surface),
UiQuad::solid(bx, footer_top, box_w, s(1.0), 0.0, skin.hairline),
];
input_quads(
&mut quads,
@@ -191,31 +224,33 @@ impl Display {
scale,
);
}
quads.push(UiQuad::solid(
auth_track.0 - s(1.0),
auth_track.1 - s(1.0),
auth_track.2 + s(2.0),
auth_track.3 + s(2.0),
s(9.0),
skin.hairline,
));
quads.push(UiQuad::solid(
auth_track.0,
auth_track.1,
auth_track.2,
auth_track.3,
s(8.0),
skin.surface,
));
for (mode, rect) in auth {
let active = editor.auth == mode;
let hovered = self.nebula_ssh_editor_hover == SshEditorHit::Auth(mode);
quads.push(UiQuad::solid(
rect.0,
rect.1,
rect.2,
rect.3,
s(7.0),
if active {
skin.accent_soft
} else if hovered {
skin.hover
} else {
skin.input
},
));
if active {
if active || hovered {
quads.push(UiQuad::solid(
rect.0,
rect.1 + rect.3 - s(2.0),
rect.1,
rect.2,
s(2.0),
0.0,
accent,
rect.3,
s(6.0),
if active { skin.hover_strong } else { skin.hover },
));
}
}
@@ -248,6 +283,14 @@ impl Display {
));
}
if show_keys {
quads.push(UiQuad::solid(
add_private_key.0 - s(1.0),
add_private_key.1 - s(1.0),
add_private_key.2 + s(2.0),
add_private_key.3 + s(2.0),
s(7.0),
skin.hairline,
));
quads.push(UiQuad::solid(
add_private_key.0,
add_private_key.1,
@@ -257,7 +300,7 @@ impl Display {
if self.nebula_ssh_editor_hover == SshEditorHit::AddPrivateKey {
skin.hover
} else {
skin.input
skin.surface
},
));
for (index, row, remove) in &visible_keys {
@@ -283,6 +326,17 @@ impl Display {
&skin,
scale,
);
for rect in [cancel_key_rect, primary_key_rect] {
quads.push(UiQuad::solid(
rect.0 - s(1.0),
rect.1 - s(1.0),
rect.2 + s(2.0),
rect.3 + s(2.0),
s(5.0),
skin.hairline,
));
quads.push(UiQuad::solid(rect.0, rect.1, rect.2, rect.3, s(4.0), skin.input));
}
let caret_field = if editor.field == SshEditorField::Password && show_password {
SshEditorField::Password
@@ -316,7 +370,7 @@ impl Display {
&size,
bx + pad,
by + s(20.0),
1.35,
1.15,
skin.ink_strong,
Flags::empty(),
if editor.original_destination.is_some() {
@@ -330,12 +384,22 @@ impl Display {
&size,
destination.0,
destination.1 - cell_h - s(5.0),
skin.ink,
"连接地址",
glyph_cache,
);
self.renderer.draw_doc_text(
&size,
destination.0 + s(82.0),
destination.1 - cell_h - s(3.0),
0.72,
if editor.error.is_some() {
if skin.is_light { Rgb::new(207, 34, 46) } else { Rgb::new(248, 81, 73) }
} else {
skin.ink_dim
},
editor.error.as_deref().unwrap_or("用户名@地址(非 22 端口可用 ssh://user@host:port)"),
Flags::empty(),
editor.error.as_deref().unwrap_or("user@host · 非 22 端口用 ssh://"),
glyph_cache,
);
self.renderer.draw_chrome_text(
@@ -360,7 +424,11 @@ impl Display {
&size,
rect.0 + (rect.2 - text_width(label)) * 0.5,
rect.1 + (rect.3 - cell_h) / 2.0,
if editor.auth == auth_mode_for_label(label) { skin.ink_strong } else { skin.ink },
if editor.auth == auth_mode_for_label(label) {
skin.ink_strong
} else {
skin.ink_dim
},
label,
glyph_cache,
);
@@ -456,10 +524,15 @@ impl Display {
&size,
cancel,
primary,
cancel_label,
primary_label,
cancel_group_x,
primary_group_x,
cancel_key_rect,
primary_key_rect,
cancel_action,
primary_action,
cancel_key,
primary_key,
cell_h,
&text_width,
&skin,
);
@@ -501,14 +574,15 @@ fn input_quads(
rect.3 + s(2.0),
s(7.0),
if active {
accent
} else if hovered {
skin.hover_strong
Rgba::new(accent.r, accent.g, accent.b, if skin.is_light { 118 } else { 136 })
} else {
skin.hairline
},
));
quads.push(UiQuad::solid(rect.0, rect.1, rect.2, rect.3, s(6.0), skin.input));
if hovered && !active {
quads.push(UiQuad::solid(rect.0, rect.1, rect.2, rect.3, s(6.0), skin.hover));
}
}
fn button_quads(
@@ -521,7 +595,8 @@ fn button_quads(
scale: f32,
) {
let s = |value: f32| value * scale;
for (rect, edge) in [(cancel, skin.hairline), (primary, accent)] {
let primary_edge = Rgba::new(accent.r, accent.g, accent.b, if skin.is_light { 72 } else { 92 });
for (rect, edge) in [(cancel, skin.hairline), (primary, primary_edge)] {
quads.push(UiQuad::solid(
rect.0 - s(1.0),
rect.1 - s(1.0),
@@ -531,20 +606,13 @@ fn button_quads(
edge,
));
}
quads.push(UiQuad::solid(cancel.0, cancel.1, cancel.2, cancel.3, s(8.0), skin.input));
quads.push(UiQuad::solid(primary.0, primary.1, primary.2, primary.3, s(8.0), accent));
quads.push(UiQuad::solid(cancel.0, cancel.1, cancel.2, cancel.3, s(8.0), skin.surface));
quads.push(UiQuad::solid(primary.0, primary.1, primary.2, primary.3, s(8.0), skin.accent_soft));
if hover == SshEditorHit::Cancel {
quads.push(UiQuad::solid(cancel.0, cancel.1, cancel.2, cancel.3, s(8.0), skin.hover));
}
if hover == SshEditorHit::Primary {
quads.push(UiQuad::solid(
primary.0,
primary.1,
primary.2,
primary.3,
s(8.0),
skin.hover_strong,
));
quads.push(UiQuad::solid(primary.0, primary.1, primary.2, primary.3, s(8.0), skin.hover));
}
}
@@ -661,26 +729,48 @@ fn draw_button_text(
size: &SizeInfo,
cancel: Rect,
primary: Rect,
cancel_label: &str,
primary_label: &str,
cancel_group_x: f32,
primary_group_x: f32,
cancel_key_rect: Rect,
primary_key_rect: Rect,
cancel_action: &str,
primary_action: &str,
cancel_key: &str,
primary_key: &str,
cell_h: f32,
text_width: &impl Fn(&str) -> f32,
skin: &theme::Skin,
) {
renderer.draw_chrome_text(
size,
cancel.0 + (cancel.2 - text_width(cancel_label)) * 0.5,
cancel_group_x,
cancel.1 + (cancel.3 - cell_h) / 2.0,
skin.ink,
cancel_label,
cancel_action,
glyph_cache,
);
renderer.draw_chrome_text(
size,
primary.0 + (primary.2 - text_width(primary_label)) * 0.5,
primary_group_x,
primary.1 + (primary.3 - cell_h) / 2.0,
skin.ink_on_accent,
primary_label,
skin.ink_strong,
primary_action,
glyph_cache,
);
renderer.draw_chrome_text(
size,
cancel_key_rect.0 + (cancel_key_rect.2 - size.cell_width() * cancel_key.len() as f32) * 0.5,
cancel_key_rect.1 + (cancel_key_rect.3 - cell_h) / 2.0,
skin.ink_dim,
cancel_key,
glyph_cache,
);
renderer.draw_chrome_text(
size,
primary_key_rect.0
+ (primary_key_rect.2 - size.cell_width() * primary_key.len() as f32) * 0.5,
primary_key_rect.1 + (primary_key_rect.3 - cell_h) / 2.0,
skin.ink_dim,
primary_key,
glyph_cache,
);
}
+29 -18
View File
@@ -1,10 +1,10 @@
use std::borrow::Cow;
use std::collections::HashSet;
use std::ffi::{CStr, CString};
use std::path::Path;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fmt, ptr};
use std::path::Path;
use ahash::RandomState;
use crossfont::Metrics;
@@ -22,17 +22,17 @@ use crate::display::SizeInfo;
use crate::display::color::Rgb;
use crate::display::content::RenderableCell;
use crate::gl;
use crate::renderer::image::ImageRenderer;
use crate::renderer::rects::{RectRenderer, RenderRect};
use crate::renderer::shader::ShaderError;
use crate::renderer::image::ImageRenderer;
use crate::renderer::ui::{UiQuad, UiRenderer};
pub mod image;
pub mod platform;
pub mod rects;
pub mod ui;
mod shader;
mod text;
pub mod ui;
pub(crate) use text::Rasterizer;
pub use text::{GlyphCache, LoaderApi};
@@ -302,12 +302,8 @@ impl Renderer {
let scale_y = -2.0 / h * mult;
let (id, u_projection) = match &self.text_renderer {
TextRendererProvider::Gles2(r) => {
(r.program().id(), r.program().projection_uniform())
},
TextRendererProvider::Glsl3(r) => {
(r.program().id(), r.program().projection_uniform())
},
TextRendererProvider::Gles2(r) => (r.program().id(), r.program().projection_uniform()),
TextRendererProvider::Glsl3(r) => (r.program().id(), r.program().projection_uniform()),
};
unsafe {
@@ -364,8 +360,7 @@ impl Renderer {
if width == 0 {
return None; // combining/zero-width marks have no cell of their own
}
let flags =
if width == 2 { Flags::WIDE_CHAR | style } else { style };
let flags = if width == 2 { Flags::WIDE_CHAR | style } else { style };
let cell = RenderableCell {
point: Point::new(0, Column(col)),
character,
@@ -453,9 +448,27 @@ impl Renderer {
style: Flags,
text: &str,
glyph_cache: &mut GlyphCache,
) -> f32 {
self.draw_doc_text_tracked(size_info, x, y, scale, 0.0, fg, style, text, glyph_cache)
}
/// Document text with explicit physical-pixel tracking between glyphs.
/// This is reserved for compact UI labels; terminal cells must remain fixed.
#[allow(clippy::too_many_arguments)]
pub fn draw_doc_text_tracked(
&mut self,
size_info: &SizeInfo,
x: f32,
y: f32,
scale: f32,
tracking: f32,
fg: Rgb,
style: Flags,
text: &str,
glyph_cache: &mut GlyphCache,
) -> f32 {
let cell_w = size_info.cell_width();
if (scale - 1.0).abs() < 0.01 {
if (scale - 1.0).abs() < 0.01 && tracking.abs() < 0.01 {
self.draw_chrome_text_styled(size_info, x, y, fg, style, text, glyph_cache);
let cols: usize = text.chars().map(|c| c.width().unwrap_or(0)).sum();
return cols as f32 * cell_w;
@@ -473,8 +486,7 @@ impl Renderer {
// scaled run's baseline lands where a truly scaled cell would put it:
// Δ = (cell_h + descent) · (scale − 1) — i.e. ascent · (scale − 1).
let metrics = glyph_cache.font_metrics();
let anchor_y =
(y + (size_info.cell_height() + metrics.descent) * (scale - 1.0)).round();
let anchor_y = (y + (size_info.cell_height() + metrics.descent) * (scale - 1.0)).round();
// Advance by the UNfloored design advance, scaled. The grid's
// `cell_width` is `floor(average_advance)`; at base size the small-ppem
@@ -502,7 +514,7 @@ impl Renderer {
underline: fg,
};
self.draw_cells(size_info, glyph_cache, std::iter::once(cell));
pen_x += width as f32 * advance;
pen_x += width as f32 * advance + tracking;
}
glyph_cache.font_size = base_size;
@@ -797,9 +809,8 @@ mod tests {
#[test]
fn asymmetric_bottom_padding_is_the_cell_viewport_origin() {
let size = SizeInfo::new_fully_asymmetric(
1000.0, 1000.0, 10.0, 20.0, 260.0, 20.0, 64.0, 16.0,
);
let size =
SizeInfo::new_fully_asymmetric(1000.0, 1000.0, 10.0, 20.0, 260.0, 20.0, 64.0, 16.0);
assert_eq!(cell_viewport(&size, 1000.0), (260, 16, 720, 920));
// Startup calls resize before the renderer has recorded the window height.