use crate::event::Rect;
use crate::types::{Color, Modifiers, WidgetId};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusBar {
pub id: WidgetId,
pub left_segments: Vec<StatusBarSegment>,
pub right_segments: Vec<StatusBarSegment>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusBarSegment {
pub text: String,
pub fg: Color,
pub bg: Color,
#[serde(default)]
pub bold: bool,
#[serde(default)]
pub action_id: Option<WidgetId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusBarHitRegion {
pub col: u16,
pub width: u16,
pub id: WidgetId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum StatusBarEvent {
SegmentClicked { id: WidgetId },
KeyPressed { key: String, modifiers: Modifiers },
}
impl StatusBar {
fn hit_regions_impl(&self, bar_width: usize) -> Vec<StatusBarHitRegion> {
let mut regions = Vec::new();
let mut col: u16 = 0;
for seg in &self.left_segments {
let w = seg.text.chars().count() as u16;
if let Some(id) = &seg.action_id {
regions.push(StatusBarHitRegion {
col,
width: w,
id: *id,
});
}
col += w;
}
let right_width: usize = self
.right_segments
.iter()
.map(|s| s.text.chars().count())
.sum();
let mut col = bar_width.saturating_sub(right_width) as u16;
for seg in &self.right_segments {
let w = seg.text.chars().count() as u16;
if let Some(id) = &seg.action_id {
regions.push(StatusBarHitRegion {
col,
width: w,
id: *id,
});
}
col += w;
}
regions
}
pub fn resolve_click(&self, click_col: u16, bar_width: usize) -> Option<WidgetId> {
for region in self.hit_regions_impl(bar_width) {
if click_col >= region.col && click_col < region.col + region.width {
return Some(region.id);
}
}
None
}
pub fn fit_right_start<F>(&self, bar_width: usize, min_gap: usize, measure: F) -> usize
where
F: Fn(&StatusBarSegment) -> usize,
{
if self.right_segments.is_empty() {
return 0;
}
let left_w: usize = self.left_segments.iter().map(&measure).sum();
let widths: Vec<usize> = self.right_segments.iter().map(&measure).collect();
let total: usize = widths.iter().sum();
if left_w + min_gap + total <= bar_width {
return 0;
}
let max_right = bar_width.saturating_sub(left_w + min_gap);
let mut remaining = total;
let last = widths.len() - 1;
for (i, w) in widths.iter().enumerate() {
if remaining <= max_right {
return i;
}
if i == last {
return i;
}
remaining -= w;
}
last
}
pub fn fit_right_start_chars(&self, bar_width: usize, min_gap: usize) -> usize {
self.fit_right_start(bar_width, min_gap, |seg| seg.text.chars().count())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StatusSegmentMeasure {
pub width: f32,
}
impl StatusSegmentMeasure {
pub fn new(width: f32) -> Self {
Self { width }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusSegmentSide {
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VisibleStatusSegment {
pub segment_idx: usize,
pub side: StatusSegmentSide,
pub bounds: Rect,
pub clickable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StatusBarHit {
Segment(WidgetId),
Empty,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatusBarLayout {
pub bar_width: f32,
pub bar_height: f32,
pub visible_segments: Vec<VisibleStatusSegment>,
pub hit_regions: Vec<(Rect, StatusBarHit)>,
pub resolved_right_start: usize,
}
impl StatusBarLayout {
pub fn hit_test(&self, x: f32, y: f32) -> StatusBarHit {
for (rect, hit) in &self.hit_regions {
if x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height {
return hit.clone();
}
}
StatusBarHit::Empty
}
}
pub const PIXEL_EDGE_INSET: f32 = 10.0;
pub const PIXEL_SEGMENT_PADDING: f32 = 5.0;
impl StatusBar {
pub fn layout<F>(
&self,
bar_width: f32,
bar_height: f32,
min_gap: f32,
measure: F,
) -> StatusBarLayout
where
F: Fn(&StatusBarSegment) -> StatusSegmentMeasure,
{
self.layout_padded(bar_width, bar_height, min_gap, 0.0, 0.0, measure)
}
pub fn layout_padded<F>(
&self,
bar_width: f32,
bar_height: f32,
min_gap: f32,
edge_inset: f32,
segment_padding: f32,
measure: F,
) -> StatusBarLayout
where
F: Fn(&StatusBarSegment) -> StatusSegmentMeasure,
{
let pad = 2.0 * segment_padding;
let mut visible_segments: Vec<VisibleStatusSegment> = Vec::new();
let mut hit_regions: Vec<(Rect, StatusBarHit)> = Vec::new();
let mut cursor = edge_inset;
for (i, seg) in self.left_segments.iter().enumerate() {
let w = measure(seg).width + pad;
let bounds = Rect::new(cursor, 0.0, w, bar_height);
let clickable = seg.action_id.is_some();
visible_segments.push(VisibleStatusSegment {
segment_idx: i,
side: StatusSegmentSide::Left,
bounds,
clickable,
});
if let Some(id) = &seg.action_id {
hit_regions.push((bounds, StatusBarHit::Segment(*id)));
}
cursor += w;
}
let left_w = cursor;
let right_widths: Vec<f32> = self
.right_segments
.iter()
.map(|s| measure(s).width + pad)
.collect();
let total_right: f32 = right_widths.iter().sum();
let max_right = (bar_width - left_w - min_gap - edge_inset).max(0.0);
let resolved_right_start =
if self.right_segments.is_empty() || total_right <= max_right + f32::EPSILON {
0
} else {
let last = right_widths.len() - 1;
let mut remaining = total_right;
let mut found = last;
for (i, w) in right_widths.iter().enumerate() {
if remaining <= max_right + f32::EPSILON {
found = i;
break;
}
if i == last {
found = i;
break;
}
remaining -= w;
}
found
};
let visible_right = &self.right_segments[resolved_right_start..];
let visible_right_widths = &right_widths[resolved_right_start..];
let total_visible: f32 = visible_right_widths.iter().sum();
let mut cursor = (bar_width - edge_inset - total_visible).max(0.0);
for (offset, seg) in visible_right.iter().enumerate() {
let seg_idx = resolved_right_start + offset;
let w = visible_right_widths[offset];
let bounds = Rect::new(cursor, 0.0, w, bar_height);
let clickable = seg.action_id.is_some();
visible_segments.push(VisibleStatusSegment {
segment_idx: seg_idx,
side: StatusSegmentSide::Right,
bounds,
clickable,
});
if let Some(id) = &seg.action_id {
hit_regions.push((bounds, StatusBarHit::Segment(*id)));
}
cursor += w;
}
StatusBarLayout {
bar_width,
bar_height,
visible_segments,
hit_regions,
resolved_right_start,
}
}
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[allow(dead_code)]
pub(crate) mod native_surface_paint {
use super::{
StatusBar, StatusBarLayout, StatusSegmentMeasure, StatusSegmentSide, PIXEL_EDGE_INSET,
PIXEL_SEGMENT_PADDING,
};
use crate::event::Rect;
use crate::paint_surface::PaintSurface;
use crate::theme::Theme;
use crate::types::WidgetId;
const MIN_GAP: f32 = 16.0;
#[allow(clippy::too_many_arguments)]
pub(crate) fn paint(
bar: &StatusBar,
surface: &mut dyn PaintSurface,
theme: &Theme,
x: f32,
y: f32,
width: f32,
line_height: f32,
hovered_id: Option<&WidgetId>,
pressed_id: Option<&WidgetId>,
) -> StatusBarLayout {
if width <= 0.0 || line_height <= 0.0 {
return bar.layout_padded(
width.max(0.0),
line_height.max(0.0),
MIN_GAP,
PIXEL_EDGE_INSET,
PIXEL_SEGMENT_PADDING,
|_| StatusSegmentMeasure::new(0.0),
);
}
let rect = Rect::new(x, y, width, line_height);
surface.surface_push_clip(rect);
let fill = bar
.left_segments
.first()
.or(bar.right_segments.first())
.map(|s| s.bg)
.unwrap_or(theme.background);
surface.surface_fill_rect(rect, fill);
let bar_layout = bar.layout_padded(
width,
line_height,
MIN_GAP,
PIXEL_EDGE_INSET,
PIXEL_SEGMENT_PADDING,
|seg| {
let (w, _) = surface.surface_measure_text_styled(&seg.text, seg.bold);
StatusSegmentMeasure::new(w)
},
);
for vs in &bar_layout.visible_segments {
let seg = match vs.side {
StatusSegmentSide::Left => &bar.left_segments[vs.segment_idx],
StatusSegmentSide::Right => &bar.right_segments[vs.segment_idx],
};
let seg_rect = Rect::new(
x + vs.bounds.x,
y + vs.bounds.y,
vs.bounds.width,
vs.bounds.height,
);
let effective_bg = if seg
.action_id
.as_ref()
.is_some_and(|id| Some(id) == pressed_id)
{
seg.bg.darken(0.05)
} else if seg
.action_id
.as_ref()
.is_some_and(|id| Some(id) == hovered_id)
{
seg.bg.lighten(0.05)
} else {
seg.bg
};
surface.surface_fill_rect(seg_rect, effective_bg);
let text_rect = Rect::new(
seg_rect.x + PIXEL_SEGMENT_PADDING,
seg_rect.y,
(seg_rect.width - 2.0 * PIXEL_SEGMENT_PADDING).max(0.0),
seg_rect.height,
);
surface.surface_draw_text_run_styled(
text_rect, &seg.text, seg.fg, seg.bold, false, false, 1.0,
);
}
surface.surface_pop_clip();
bar_layout
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::ImagePaintResult;
use crate::event::Viewport;
use crate::primitives::status_bar::{StatusBarHit, StatusBarSegment};
use crate::types::Color;
use crate::Image;
#[derive(Default)]
struct RecordingSurface {
fills: Vec<(Rect, Color)>,
text_runs: Vec<(Rect, String, Color, bool)>,
clip_pushes: Vec<Rect>,
clip_pops: usize,
}
const BOLD_BONUS_PX: f32 = 100.0;
impl PaintSurface for RecordingSurface {
fn surface_begin_frame(&mut self, _viewport: Viewport) {}
fn surface_end_frame(&mut self) {}
fn surface_viewport(&self) -> Viewport {
Viewport::new(200.0, 100.0, 1.0)
}
fn surface_line_height(&self) -> f32 {
16.0
}
fn surface_char_width(&self) -> f32 {
8.0
}
fn surface_measure_text(&self, text: &str) -> (f32, f32) {
(text.chars().count() as f32 * 8.0, 16.0)
}
fn surface_measure_text_styled(&self, text: &str, bold: bool) -> (f32, f32) {
let (w, h) = self.surface_measure_text(text);
(w + if bold { BOLD_BONUS_PX } else { 0.0 }, h)
}
fn surface_fill_rect(&mut self, rect: Rect, color: Color) {
self.fills.push((rect, color));
}
fn surface_fill_rounded_rect(&mut self, rect: Rect, _radius: f32, color: Color) {
self.fills.push((rect, color));
}
fn surface_stroke_rect(&mut self, _rect: Rect, _color: Color, _stroke_width: f32) {}
fn surface_stroke_rounded_rect(
&mut self,
_rect: Rect,
_radius: f32,
_color: Color,
_stroke_width: f32,
) {
}
fn surface_draw_text_run(&mut self, rect: Rect, text: &str, color: Color) {
self.text_runs.push((rect, text.to_string(), color, false));
}
#[allow(clippy::too_many_arguments)]
fn surface_draw_text_run_styled(
&mut self,
rect: Rect,
text: &str,
color: Color,
bold: bool,
_italic: bool,
_underline: bool,
_scale_x: f32,
) {
self.text_runs.push((rect, text.to_string(), color, bold));
}
fn surface_draw_line(
&mut self,
_from: crate::Point,
_to: crate::Point,
_color: Color,
_stroke_width: f32,
) {
}
fn surface_push_clip(&mut self, rect: Rect) {
self.clip_pushes.push(rect);
}
fn surface_pop_clip(&mut self) {
self.clip_pops += 1;
}
fn surface_draw_image(&mut self, _rect: Rect, _image: &Image) -> ImagePaintResult {
ImagePaintResult::Unsupported
}
}
fn sample_bar() -> StatusBar {
StatusBar {
id: WidgetId::new("sb"),
left_segments: vec![StatusBarSegment {
text: "NORMAL".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(10, 20, 30),
bold: true,
action_id: Some(WidgetId::new("sb:mode")),
}],
right_segments: vec![StatusBarSegment {
text: "Ln 1, Col 1".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(40, 50, 60),
bold: false,
action_id: Some(WidgetId::new("sb:cursor")),
}],
}
}
#[test]
fn bar_background_falls_back_to_theme_when_no_segments() {
let bar = StatusBar {
id: WidgetId::new("empty"),
left_segments: vec![],
right_segments: vec![],
};
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(
&bar,
&mut surface,
&theme,
0.0,
0.0,
200.0,
20.0,
None,
None,
);
assert_eq!(surface.fills[0].1, theme.background);
}
#[test]
fn bar_background_uses_first_segment_bg_when_present() {
let bar = sample_bar();
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(
&bar,
&mut surface,
&theme,
0.0,
0.0,
200.0,
20.0,
None,
None,
);
assert_eq!(surface.fills[0].1, bar.left_segments[0].bg);
}
#[test]
fn zero_width_short_circuits_without_touching_surface() {
let bar = sample_bar();
let theme = Theme::default();
let mut surface = RecordingSurface::default();
let layout = paint(&bar, &mut surface, &theme, 0.0, 0.0, 0.0, 20.0, None, None);
assert!(surface.fills.is_empty());
assert!(surface.text_runs.is_empty());
assert!(surface.clip_pushes.is_empty());
assert_eq!(surface.clip_pops, 0);
assert!(!layout.visible_segments.is_empty());
}
#[test]
fn clip_is_pushed_and_popped_around_the_bar_rect() {
let bar = sample_bar();
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(
&bar,
&mut surface,
&theme,
5.0,
3.0,
200.0,
20.0,
None,
None,
);
assert_eq!(surface.clip_pushes, vec![Rect::new(5.0, 3.0, 200.0, 20.0)]);
assert_eq!(surface.clip_pops, 1);
}
#[test]
fn bold_segment_measured_wider_than_the_same_text_plain() {
let mut bar = sample_bar();
bar.left_segments[0].bold = false;
let theme = Theme::default();
let mut plain_surface = RecordingSurface::default();
let plain_layout = paint(
&bar,
&mut plain_surface,
&theme,
0.0,
0.0,
200.0,
20.0,
None,
None,
);
bar.left_segments[0].bold = true;
let mut bold_surface = RecordingSurface::default();
let bold_layout = paint(
&bar,
&mut bold_surface,
&theme,
0.0,
0.0,
200.0,
20.0,
None,
None,
);
let plain_w = plain_layout.visible_segments[0].bounds.width;
let bold_w = bold_layout.visible_segments[0].bounds.width;
assert!(
(bold_w - plain_w - BOLD_BONUS_PX).abs() < 0.01,
"bold width {bold_w} should exceed plain width {plain_w} by exactly the bold bonus"
);
}
#[test]
fn text_drawn_via_styled_run_with_segment_bold_flag() {
let bar = sample_bar();
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(
&bar,
&mut surface,
&theme,
0.0,
0.0,
200.0,
20.0,
None,
None,
);
let (_, _, _, left_bold) = surface
.text_runs
.iter()
.find(|(_, t, _, _)| t == "NORMAL")
.expect("left segment text drawn");
assert!(*left_bold, "left segment is bold=true");
let (_, _, _, right_bold) = surface
.text_runs
.iter()
.find(|(_, t, _, _)| t == "Ln 1, Col 1")
.expect("right segment text drawn");
assert!(!*right_bold, "right segment is bold=false");
}
#[test]
fn hover_lightens_and_press_darkens_clickable_segment_bg() {
let bar = sample_bar();
let theme = Theme::default();
let base = bar.left_segments[0].bg;
let hovered = WidgetId::new("sb:mode");
let mut hovered_surface = RecordingSurface::default();
paint(
&bar,
&mut hovered_surface,
&theme,
0.0,
0.0,
200.0,
20.0,
Some(&hovered),
None,
);
assert_eq!(hovered_surface.fills[1].1, base.lighten(0.05));
let pressed = WidgetId::new("sb:mode");
let mut pressed_surface = RecordingSurface::default();
paint(
&bar,
&mut pressed_surface,
&theme,
0.0,
0.0,
200.0,
20.0,
None,
Some(&pressed),
);
assert_eq!(pressed_surface.fills[1].1, base.darken(0.05));
}
#[test]
fn paint_and_click_round_trip() {
let bar = sample_bar();
let theme = Theme::default();
let mut surface = RecordingSurface::default();
let layout = paint(
&bar,
&mut surface,
&theme,
0.0,
0.0,
400.0,
20.0,
None,
None,
);
let mode = layout
.visible_segments
.iter()
.find(|vs| vs.side == StatusSegmentSide::Left)
.expect("mode segment visible");
let hit = layout.hit_test(mode.bounds.x + 1.0, mode.bounds.y + 1.0);
assert_eq!(hit, StatusBarHit::Segment(WidgetId::new("sb:mode")));
let cursor = layout
.visible_segments
.iter()
.find(|vs| vs.side == StatusSegmentSide::Right)
.expect("cursor segment visible");
let hit = layout.hit_test(cursor.bounds.x + 1.0, cursor.bounds.y + 1.0);
assert_eq!(hit, StatusBarHit::Segment(WidgetId::new("sb:cursor")));
}
#[test]
fn paint_reserves_pixel_edge_inset_at_both_bar_edges() {
let bar = sample_bar();
let theme = Theme::default();
let mut surface = RecordingSurface::default();
let layout = paint(
&bar,
&mut surface,
&theme,
0.0,
0.0,
400.0,
20.0,
None,
None,
);
let left = layout
.visible_segments
.iter()
.find(|vs| vs.side == StatusSegmentSide::Left)
.expect("left segment visible");
assert_eq!(
left.bounds.x, PIXEL_EDGE_INSET,
"left-most segment should start `PIXEL_EDGE_INSET` in from x=0, \
not flush at the bar's own left edge"
);
let right = layout
.visible_segments
.iter()
.find(|vs| vs.side == StatusSegmentSide::Right)
.expect("right segment visible");
assert_eq!(
right.bounds.x + right.bounds.width,
400.0 - PIXEL_EDGE_INSET,
"right-most segment should end `PIXEL_EDGE_INSET` short of the \
bar's own right edge, not touching it"
);
}
#[test]
fn paint_insets_text_within_the_padded_segment_box_but_fills_the_whole_box() {
let bar = sample_bar();
let theme = Theme::default();
let mut surface = RecordingSurface::default();
let layout = paint(
&bar,
&mut surface,
&theme,
0.0,
0.0,
400.0,
20.0,
None,
None,
);
let left = layout
.visible_segments
.iter()
.find(|vs| vs.side == StatusSegmentSide::Left)
.expect("left segment visible");
let (fill_rect, _) = surface
.fills
.iter()
.find(|(r, _)| *r == left.bounds)
.expect("segment background filled across its full padded bounds");
let (text_rect, _, _, _) = surface
.text_runs
.iter()
.find(|(_, t, _, _)| t == "NORMAL")
.expect("left segment text drawn");
assert_eq!(
text_rect.x,
fill_rect.x + PIXEL_SEGMENT_PADDING,
"text should be inset by `PIXEL_SEGMENT_PADDING` from the \
filled segment box's own left edge"
);
assert!(
text_rect.width < fill_rect.width,
"text draw rect ({}) should be narrower than the filled \
segment box ({}) it sits inside",
text_rect.width,
fill_rect.width
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_bar_roundtrip_serde() {
let bar = StatusBar {
id: WidgetId::new("editor-status"),
left_segments: vec![
StatusBarSegment {
text: " NORMAL ".to_string(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(30, 30, 30),
bold: true,
action_id: None,
},
StatusBarSegment {
text: " main.rs".to_string(),
fg: Color::rgb(200, 200, 200),
bg: Color::rgb(30, 30, 30),
bold: true,
action_id: None,
},
],
right_segments: vec![
StatusBarSegment {
text: " rust ".to_string(),
fg: Color::rgb(200, 200, 200),
bg: Color::rgb(30, 30, 30),
bold: false,
action_id: Some(WidgetId::new("status:change_language")),
},
StatusBarSegment {
text: " Ln 12, Col 4 ".to_string(),
fg: Color::rgb(200, 200, 200),
bg: Color::rgb(30, 30, 30),
bold: false,
action_id: Some(WidgetId::new("status:goto_line")),
},
],
};
let json = serde_json::to_string(&bar).unwrap();
let back: StatusBar = serde_json::from_str(&json).unwrap();
assert_eq!(bar, back);
}
#[test]
fn status_bar_resolve_click() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![StatusBarSegment {
text: " LEFT ".to_string(),
fg: Color::rgb(0, 0, 0),
bg: Color::rgb(0, 0, 0),
bold: false,
action_id: Some(WidgetId::new("left")),
}],
right_segments: vec![StatusBarSegment {
text: " R ".to_string(),
fg: Color::rgb(0, 0, 0),
bg: Color::rgb(0, 0, 0),
bold: false,
action_id: Some(WidgetId::new("right")),
}],
};
assert_eq!(
bar.resolve_click(3, 30).as_ref().map(|w| w.as_str()),
Some("left")
);
assert_eq!(
bar.resolve_click(28, 30).as_ref().map(|w| w.as_str()),
Some("right")
);
assert_eq!(bar.resolve_click(15, 30), None); }
#[test]
fn status_bar_fit_right_start_chars() {
let mk = |text: &str, id: &str| StatusBarSegment {
text: text.to_string(),
fg: Color::rgb(0, 0, 0),
bg: Color::rgb(0, 0, 0),
bold: false,
action_id: Some(WidgetId::new(id)),
};
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![mk(" LEFT", "left")],
right_segments: vec![
mk(" a ", "lo0"),
mk(" b ", "lo1"),
mk(" c ", "lo2"),
mk(" d ", "lo3"),
mk(" Ln 1,Col 1", "cursor"),
],
};
assert_eq!(bar.fit_right_start_chars(40, 2), 0);
assert_eq!(bar.fit_right_start_chars(30, 2), 0);
assert_eq!(bar.fit_right_start_chars(29, 2), 1);
assert_eq!(bar.fit_right_start_chars(20, 2), 4);
assert_eq!(bar.fit_right_start_chars(5, 2), 4);
let empty_right = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![mk(" X", "x")],
right_segments: vec![],
};
assert_eq!(empty_right.fit_right_start_chars(10, 2), 0);
}
#[test]
fn status_bar_fit_right_start_generic_pixel_measurer() {
let mk = |text: &str, id: &str| StatusBarSegment {
text: text.to_string(),
fg: Color::rgb(0, 0, 0),
bg: Color::rgb(0, 0, 0),
bold: false,
action_id: Some(WidgetId::new(id)),
};
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![mk("LL", "left")], right_segments: vec![
mk("aaa", "lo"), mk("bbbb", "mid"), mk("cursor", "hi"), ],
};
let measure_px = |seg: &StatusBarSegment| seg.text.chars().count() * 10;
assert_eq!(bar.fit_right_start(200, 16, measure_px), 0);
assert_eq!(bar.fit_right_start(150, 16, measure_px), 1);
assert_eq!(bar.fit_right_start(100, 16, measure_px), 2);
assert_eq!(bar.fit_right_start(30, 16, measure_px), 2);
let bold = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![StatusBarSegment {
text: "BOLD".to_string(),
fg: Color::rgb(0, 0, 0),
bg: Color::rgb(0, 0, 0),
bold: true,
action_id: None,
}],
right_segments: vec![mk("xx", "a"), mk("yy", "b")],
};
let measure_with_bold =
|seg: &StatusBarSegment| seg.text.chars().count() * 10 + if seg.bold { 5 } else { 0 };
assert_eq!(bold.fit_right_start(90, 5, measure_with_bold), 0);
assert_eq!(bold.fit_right_start(89, 5, measure_with_bold), 1);
}
fn make_status_seg(text: &str, id: Option<&str>, bold: bool) -> StatusBarSegment {
StatusBarSegment {
text: text.to_string(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(30, 30, 30),
bold,
action_id: id.map(WidgetId::new),
}
}
#[test]
fn status_bar_layout_empty() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![],
right_segments: vec![],
};
let layout = bar.layout(30.0, 1.0, 2.0, |_| StatusSegmentMeasure::new(0.0));
assert_eq!(layout.visible_segments.len(), 0);
assert_eq!(layout.hit_regions.len(), 0);
assert_eq!(layout.resolved_right_start, 0);
assert_eq!(layout.hit_test(5.0, 0.5), StatusBarHit::Empty);
}
#[test]
fn status_bar_layout_left_only() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![
make_status_seg(" NORMAL ", None, true),
make_status_seg(" main.rs", Some("filename"), false),
],
right_segments: vec![],
};
let layout = bar.layout(50.0, 1.0, 2.0, |seg| {
StatusSegmentMeasure::new(seg.text.chars().count() as f32)
});
assert_eq!(layout.visible_segments.len(), 2);
assert_eq!(layout.visible_segments[0].bounds.x, 0.0);
assert_eq!(layout.visible_segments[0].bounds.width, 8.0); assert_eq!(layout.visible_segments[0].side, StatusSegmentSide::Left);
assert!(!layout.visible_segments[0].clickable);
assert_eq!(layout.visible_segments[1].bounds.x, 8.0);
assert_eq!(layout.visible_segments[1].side, StatusSegmentSide::Left);
assert!(layout.visible_segments[1].clickable);
assert_eq!(layout.hit_test(3.0, 0.5), StatusBarHit::Empty);
match layout.hit_test(10.0, 0.5) {
StatusBarHit::Segment(id) => assert_eq!(id.as_str(), "filename"),
other => panic!("expected Segment(filename), got {other:?}"),
}
}
#[test]
fn status_bar_layout_right_aligned() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![make_status_seg(" NORMAL", None, true)],
right_segments: vec![
make_status_seg(" rust ", Some("lang"), false),
make_status_seg(" Ln 1,Col 1 ", Some("cursor"), false),
],
};
let layout = bar.layout(40.0, 1.0, 2.0, |seg| {
StatusSegmentMeasure::new(seg.text.chars().count() as f32)
});
assert_eq!(layout.resolved_right_start, 0);
assert_eq!(layout.visible_segments.len(), 3);
let lang = &layout.visible_segments[1];
assert_eq!(lang.side, StatusSegmentSide::Right);
assert_eq!(lang.bounds.x, 22.0);
assert_eq!(lang.bounds.width, 6.0);
let cursor = &layout.visible_segments[2];
assert_eq!(cursor.bounds.x, 28.0);
match layout.hit_test(30.0, 0.5) {
StatusBarHit::Segment(id) => assert_eq!(id.as_str(), "cursor"),
other => panic!("expected Segment(cursor), got {other:?}"),
}
}
#[test]
fn status_bar_layout_priority_drop() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![make_status_seg(" LEFT", None, false)],
right_segments: vec![
make_status_seg(" a ", Some("lo0"), false), make_status_seg(" b ", Some("lo1"), false), make_status_seg(" c ", Some("lo2"), false), make_status_seg(" Ln 1,Col 1", Some("cursor"), false), ],
};
let layout = bar.layout(20.0, 1.0, 2.0, |seg| {
StatusSegmentMeasure::new(seg.text.chars().count() as f32)
});
assert_eq!(layout.resolved_right_start, 3);
assert_eq!(layout.visible_segments.len(), 2);
let surviving_right = layout
.visible_segments
.iter()
.find(|v| v.side == StatusSegmentSide::Right)
.unwrap();
assert_eq!(surviving_right.segment_idx, 3);
assert_eq!(surviving_right.bounds.width, 11.0);
assert_eq!(layout.hit_test(7.0, 0.5), StatusBarHit::Empty);
}
#[test]
fn status_bar_layout_pixel_units_fractional() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![make_status_seg("NORMAL", None, true)],
right_segments: vec![make_status_seg("Ln 1,Col 1", Some("cursor"), false)],
};
let measure = |seg: &StatusBarSegment| {
let w = seg.text.chars().count() as f32 * 7.3 + if seg.bold { 5.0 } else { 0.0 };
StatusSegmentMeasure::new(w)
};
let layout = bar.layout(400.0, 22.0, 16.0, measure);
assert_eq!(layout.resolved_right_start, 0);
assert_eq!(layout.visible_segments.len(), 2);
assert_eq!(layout.visible_segments[0].side, StatusSegmentSide::Left);
assert_eq!(layout.visible_segments[0].bounds.x, 0.0);
assert!((layout.visible_segments[0].bounds.width - (6.0 * 7.3 + 5.0)).abs() < 0.01);
let right = &layout.visible_segments[1];
let right_w = 10.0 * 7.3;
assert!((right.bounds.x - (400.0 - right_w)).abs() < 0.01);
}
#[test]
fn status_bar_layout_always_keeps_last_right_segment() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![make_status_seg("LEFT_MORE", None, false)],
right_segments: vec![make_status_seg("cursor_info", Some("cursor"), false)],
};
let layout = bar.layout(10.0, 1.0, 2.0, |seg| {
StatusSegmentMeasure::new(seg.text.chars().count() as f32)
});
assert_eq!(layout.resolved_right_start, 0);
let r = layout
.visible_segments
.iter()
.find(|v| v.side == StatusSegmentSide::Right);
assert!(
r.is_some(),
"last segment should survive even when too wide"
);
}
#[test]
fn layout_padded_insets_left_segment_from_the_bar_edge() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![make_status_seg("NORMAL", Some("mode"), false)],
right_segments: vec![],
};
let layout = bar.layout_padded(200.0, 20.0, 16.0, 10.0, 5.0, |seg| {
StatusSegmentMeasure::new(seg.text.chars().count() as f32 * 8.0)
});
let mode = &layout.visible_segments[0];
assert_eq!(mode.bounds.x, 10.0);
assert_eq!(mode.bounds.width, 6.0 * 8.0 + 2.0 * 5.0);
}
#[test]
fn layout_padded_insets_right_segment_from_the_bar_edge() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![],
right_segments: vec![make_status_seg("Ln 1, Col 1", Some("cursor"), false)],
};
let measured_w = "Ln 1, Col 1".chars().count() as f32 * 8.0;
let layout = bar.layout_padded(200.0, 20.0, 16.0, 10.0, 5.0, |seg| {
StatusSegmentMeasure::new(seg.text.chars().count() as f32 * 8.0)
});
let cursor = &layout.visible_segments[0];
let padded_w = measured_w + 2.0 * 5.0;
assert_eq!(cursor.bounds.x + cursor.bounds.width, 200.0 - 10.0);
assert_eq!(cursor.bounds.width, padded_w);
}
#[test]
fn layout_padded_hit_region_covers_the_padded_bounds_not_just_the_text() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![make_status_seg("X", Some("mode"), false)],
right_segments: vec![],
};
let layout = bar.layout_padded(200.0, 20.0, 16.0, 10.0, 5.0, |seg| {
StatusSegmentMeasure::new(seg.text.chars().count() as f32 * 8.0)
});
match layout.hit_test(11.0, 5.0) {
StatusBarHit::Segment(id) => assert_eq!(id.as_str(), "mode"),
other => panic!("expected the padding to be part of the hit region, got {other:?}"),
}
}
#[test]
fn layout_with_zero_inset_and_padding_matches_plain_layout() {
let bar = StatusBar {
id: WidgetId::new("t"),
left_segments: vec![make_status_seg(" NORMAL ", Some("mode"), true)],
right_segments: vec![make_status_seg(" Ln 1, Col 1 ", Some("cursor"), false)],
};
let measure = |seg: &StatusBarSegment| StatusSegmentMeasure::new(seg.text.len() as f32);
let via_layout = bar.layout(80.0, 1.0, 2.0, measure);
let via_padded = bar.layout_padded(80.0, 1.0, 2.0, 0.0, 0.0, measure);
assert_eq!(via_layout, via_padded);
}
}