use strop_core::id::DocumentId;
use super::{Editor, Surface};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ViewGeometry {
pub columns: u16,
pub rows: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellRect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
pub const STANDARD_GUTTER: usize = 5;
pub const BLAME_GUTTER_WIDTH: usize = 22;
pub fn text_budget(rect: CellRect) -> CellRect {
CellRect {
width: rect.width.saturating_sub(1),
..rect
}
}
pub fn pane_cells(count: usize, is_row: bool, area: CellRect) -> Vec<CellRect> {
if count == 0 {
return Vec::new();
}
let total_w = usize::from(area.width);
let total_h = usize::from(area.height.saturating_sub(1)); let axis = if is_row { total_w } else { total_h };
let usable = axis.saturating_sub(count - 1);
let base = usable / count;
let mut offset = 0usize;
let mut rects = Vec::with_capacity(count);
for i in 0..count {
let size = if i + 1 == count {
usable - base * (count - 1)
} else {
base
};
let size = size.min(axis.saturating_sub(offset));
let offset_in_area = offset.min(axis);
let (x, y, w, h) = if is_row {
(
usize::from(area.x) + offset_in_area,
usize::from(area.y),
size,
total_h,
)
} else {
(
usize::from(area.x),
usize::from(area.y) + offset_in_area,
total_w,
size,
)
};
let mut rect = CellRect::new(x as u16, y as u16, w as u16, h as u16);
if count > 1 && h > 0 && w > 0 {
rect.y = rect.y.saturating_add(1);
rect.height = rect.height.saturating_sub(1);
}
rects.push(rect);
offset = offset.saturating_add(size).saturating_add(1);
}
rects
}
impl CellRect {
pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
Self {
x,
y,
width,
height,
}
}
}
impl From<ViewGeometry> for CellRect {
fn from(geometry: ViewGeometry) -> Self {
CellRect::new(0, 0, geometry.columns, geometry.rows)
}
}
pub fn picker_card(area: CellRect, kind: strop_picker::Kind) -> CellRect {
if matches!(
kind,
strop_picker::Kind::Search
| strop_picker::Kind::Files
| strop_picker::Kind::Symbols
| strop_picker::Kind::WorkspaceSymbols
) {
CellRect {
x: area.x + 1,
y: area.y,
width: area.width.saturating_sub(2),
height: area.height.saturating_sub(1),
}
} else {
let width = ((u32::from(area.width) * 84 / 100) as u16)
.max(50)
.min(area.width.saturating_sub(2));
let height = if kind == strop_picker::Kind::RemoteAddress {
8
} else {
((u32::from(area.height) * 70 / 100) as u16).max(12)
}
.min(area.height.saturating_sub(2));
CellRect {
x: (area.width - width) / 2,
y: (area.height - height) / 2,
width,
height,
}
}
}
pub fn picker_inner(card: CellRect) -> CellRect {
CellRect {
x: card.x + 2,
y: card.y + 1,
width: card.width.saturating_sub(4),
height: card.height.saturating_sub(2),
}
}
pub fn picker_input_height(kind: strop_picker::Kind, replace_visible: bool) -> u16 {
if kind == strop_picker::Kind::Search && replace_visible {
3
} else {
2
}
}
pub fn picker_field_split(inner: CellRect, input_h: u16) -> (CellRect, CellRect) {
let field_h = input_h.min(inner.height.saturating_sub(1));
let fields = CellRect {
height: field_h,
..inner
};
let content = CellRect {
y: inner.y + field_h,
height: inner.height.saturating_sub(field_h),
..inner
};
(fields, content)
}
fn percentage_pair(width: u16, first_pct: u16) -> (u16, u16) {
let first = ((u32::from(width) * u32::from(first_pct) + 50) / 100) as u16;
(first, width.saturating_sub(first))
}
pub fn picker_split(
area: CellRect,
kind: strop_picker::Kind,
replace_visible: bool,
) -> Option<(CellRect, Option<CellRect>)> {
let card = picker_card(area, kind);
let inner = picker_inner(card);
let input_h = picker_input_height(kind, replace_visible);
if inner.height < input_h + 1 {
return None;
}
let (_, content) = picker_field_split(inner, input_h);
let remote_picker = matches!(
kind,
strop_picker::Kind::RemoteHosts | strop_picker::Kind::RemoteAddress
);
if remote_picker {
return Some((content, None));
}
if card.width < 64 && content.height >= 12 {
let preview_h = content.height.saturating_sub(8).min(8);
let list_h = content.height - preview_h;
let list = CellRect {
height: list_h,
..content
};
let preview = CellRect {
y: content.y + list_h,
height: content.height - list_h,
..content
};
return Some((list, Some(preview)));
}
if card.width < 64 {
return Some((content, None));
}
let list_pct = if kind == strop_picker::Kind::Search {
60
} else {
55
};
let (list_w, preview_w) = percentage_pair(content.width, list_pct);
let list = CellRect {
width: list_w,
..content
};
let preview = CellRect {
x: content.x + list_w,
width: preview_w,
..content
};
Some((list, Some(preview)))
}
pub fn picker_results(
area: CellRect,
kind: strop_picker::Kind,
replace_visible: bool,
) -> Option<CellRect> {
picker_split(area, kind, replace_visible).map(|(results, _)| results)
}
pub fn picker_rows_per_result(kind: strop_picker::Kind, replace_visible: bool) -> usize {
if kind == strop_picker::Kind::Search {
if replace_visible {
3
} else {
2
}
} else {
1
}
}
pub fn gutter_width(surface: Option<&Surface>) -> usize {
let Some(Surface::Diff { hunks, .. }) = surface else {
return STANDARD_GUTTER;
};
hunks.gutter_width()
}
impl Editor {
pub fn number_gutter_width(&self, doc: DocumentId) -> usize {
let buffer = self.doc(doc);
let surface = buffer.surface_payload();
if matches!(surface, Some(Surface::Diff { .. })) {
return gutter_width(surface);
}
let number = buffer.buf.last_content_line() + 1;
let digits = number.ilog10() as usize + 1;
2 + digits.max(3)
}
pub fn left_inset(&self, buffer: DocumentId) -> usize {
let surface = self.document(buffer).and_then(|d| d.surface_payload());
let mut inset = self.number_gutter_width(buffer);
if self.blame_gutter_for(buffer).is_some() {
inset += BLAME_GUTTER_WIDTH;
}
if let Some(Surface::Diff {
commit: Some(cf), ..
}) = surface
{
inset += cf.files.sidebar().outer_width();
}
inset
}
}
pub fn preview_source_lines(rope: &ropey::Rope) -> usize {
rope.len_lines()
.saturating_sub(usize::from(
rope.len_bytes() > 0 && rope.byte(rope.len_bytes() - 1) == b'\n',
))
.max(1)
}
pub fn preview_window(
rope: &ropey::Rope,
focus: Option<usize>,
visible: usize,
) -> std::ops::Range<usize> {
let first = focus
.map_or(0, |line| line.saturating_sub(1).saturating_sub(visible / 3))
.min(preview_source_lines(rope).saturating_sub(1));
first
..first
.saturating_add(visible)
.min(preview_source_lines(rope))
}