362 lines
12 KiB
Rust
362 lines
12 KiB
Rust
//! Headers, sizing drags and the fill handle (TABLE-02).
|
|
|
|
use ely_gpui_component::{primitives::FocusRing as _, theme::ActiveTheme as _};
|
|
use ely_lok::{
|
|
LokView, Rect,
|
|
sheet::{Axis, address, column_name},
|
|
};
|
|
use ely_office::prelude::*;
|
|
use gpui::{
|
|
AnyElement, ClickEvent, Context, CursorStyle, Entity, InteractiveElement as _, IntoElement,
|
|
Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _,
|
|
Pixels, Point, Role, StatefulInteractiveElement as _, Styled as _, Window, div, px,
|
|
transparent_black,
|
|
};
|
|
|
|
use crate::book::Book;
|
|
|
|
/// A drag over the grid.
|
|
pub enum Drag {
|
|
/// A column or row edge.
|
|
Size { column: bool, index: u32 },
|
|
/// The fill handle, to the cell under the pointer.
|
|
Fill { to: Option<(u32, u32)> },
|
|
}
|
|
|
|
/// The width that grabs a header edge.
|
|
const EDGE: f32 = 4.0;
|
|
const HANDLE: f32 = 7.0;
|
|
|
|
/// The vcl modifier bits a header click takes.
|
|
fn modifier(modifiers: &Modifiers) -> String {
|
|
let shift = if modifiers.shift { 0x1000 } else { 0 };
|
|
let add = if modifiers.secondary() { 0x2000 } else { 0 };
|
|
(shift | add).to_string()
|
|
}
|
|
|
|
/// Twips as 1/100 mm, as the size commands take them.
|
|
fn mm100(twips: i32) -> i32 {
|
|
(twips * 2540 + 720) / 1440
|
|
}
|
|
|
|
impl Book {
|
|
pub fn grid(&mut self, _: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
|
let tab = self.active_tab().expect("the grid needs a tab");
|
|
let view = tab.view.clone();
|
|
let (rows, columns) = (
|
|
tab.extra.geometry.rows.clone(),
|
|
tab.extra.geometry.columns.clone(),
|
|
);
|
|
let theme = cx.theme();
|
|
let (border, sunken) = (theme.colors.border, theme.colors.sunken);
|
|
let column_strip = self.strip(&view, &columns, true, cx);
|
|
let row_strip = self.strip(&view, &rows, false, cx);
|
|
let corner = div()
|
|
.w_12()
|
|
.h_6()
|
|
.border_r_1()
|
|
.border_b_1()
|
|
.border_color(border);
|
|
div()
|
|
.flex_1()
|
|
.min_h_0()
|
|
.flex()
|
|
.flex_col()
|
|
.bg(sunken)
|
|
.on_mouse_move(cx.listener(Self::on_drag_move))
|
|
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_drag_end))
|
|
.child(div().flex().child(corner).child(column_strip))
|
|
.child(
|
|
div().flex_1().min_h_0().flex().child(row_strip).child(
|
|
div()
|
|
.relative()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.child(view.clone())
|
|
.children(self.fill_handle(&view, cx))
|
|
.children(self.cell_aids(&view, cx))
|
|
.children(self.find_box(cx)),
|
|
),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
/// One header strip; `column` picks the axis.
|
|
fn strip(
|
|
&self,
|
|
view: &Entity<LokView>,
|
|
axis: &Axis,
|
|
column: bool,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let theme = cx.theme();
|
|
let border = theme.colors.border;
|
|
let read = view.read(cx);
|
|
let (width, height) = read.viewport();
|
|
let along = |twips: i32| {
|
|
let at = read.place((twips, twips));
|
|
f32::from(if column { at.x } else { at.y })
|
|
};
|
|
let length = if column { width } else { height };
|
|
let pick = |pair: (i32, i32)| if column { pair.0 } else { pair.1 };
|
|
let (frozen, first) = (pick(read.frozen()), pick(read.scrolled_start()));
|
|
let mut spans = Vec::new();
|
|
let mut take = |index: u32| {
|
|
let (start, end) = (along(axis.start(index)), along(axis.start(index + 1)));
|
|
if end > start {
|
|
spans.push((index, start, end - start));
|
|
}
|
|
};
|
|
// The frozen indexes, then the scrolled ones.
|
|
let held = if frozen > 0 {
|
|
axis.at(frozen - 1) + 1
|
|
} else {
|
|
0
|
|
};
|
|
(0..held).for_each(&mut take);
|
|
let mut index = axis.at(first).max(held);
|
|
while index < axis.count() && along(axis.start(index)) < length {
|
|
take(index);
|
|
index += 1;
|
|
}
|
|
let cells: Vec<AnyElement> = spans
|
|
.into_iter()
|
|
.map(|(index, start, size)| self.header(index, start, size, column, cx))
|
|
.collect();
|
|
let strip = div()
|
|
.id(if column {
|
|
"table-columns"
|
|
} else {
|
|
"table-rows"
|
|
})
|
|
.relative()
|
|
.overflow_hidden()
|
|
.border_color(border)
|
|
.children(cells);
|
|
if column {
|
|
strip.flex_1().h_6().border_b_1().into_any_element()
|
|
} else {
|
|
strip.w_12().h_full().border_r_1().into_any_element()
|
|
}
|
|
}
|
|
|
|
/// One row or column header with its sizing edge.
|
|
fn header(
|
|
&self,
|
|
index: u32,
|
|
start: f32,
|
|
size: f32,
|
|
column: bool,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let theme = cx.theme();
|
|
let (border, text) = (theme.colors.border, theme.colors.fg_muted);
|
|
let label = if column {
|
|
column_name(index)
|
|
} else {
|
|
(index + 1).to_string()
|
|
};
|
|
let edge = div()
|
|
.id(("table-edge", index as usize))
|
|
.absolute()
|
|
.cursor(if column {
|
|
CursorStyle::ResizeLeftRight
|
|
} else {
|
|
CursorStyle::ResizeUpDown
|
|
})
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
cx.listener(move |book, _: &MouseDownEvent, _, cx| {
|
|
cx.stop_propagation();
|
|
book.drag = Some(Drag::Size { column, index });
|
|
}),
|
|
);
|
|
let edge = if column {
|
|
edge.top_0().right_0().h_full().w(px(EDGE))
|
|
} else {
|
|
edge.left_0().bottom_0().w_full().h(px(EDGE))
|
|
};
|
|
let id = (
|
|
if column { "table-column" } else { "table-row" },
|
|
index as usize,
|
|
);
|
|
let name = if column {
|
|
Book::text(cx, "table.header.column", &[("name", &label)])
|
|
} else {
|
|
Book::text(cx, "table.header.row", &[("name", &label)])
|
|
};
|
|
let button = div()
|
|
.id(id)
|
|
.role(if column {
|
|
Role::ColumnHeader
|
|
} else {
|
|
Role::RowHeader
|
|
})
|
|
.aria_label(name)
|
|
.size_full()
|
|
.flex()
|
|
.items_center()
|
|
.justify_center()
|
|
.border_1()
|
|
.border_color(transparent_black())
|
|
.focus_ring(cx)
|
|
.tab_index(0)
|
|
.on_click(cx.listener(move |book, event: &ClickEvent, _, cx| {
|
|
let id = if column {
|
|
"select.column"
|
|
} else {
|
|
"select.row"
|
|
};
|
|
book.run_active(id, &[&index.to_string(), &modifier(&event.modifiers())], cx);
|
|
}))
|
|
.child(label);
|
|
let cell = div()
|
|
.absolute()
|
|
.overflow_hidden()
|
|
.text_xs()
|
|
.text_color(text)
|
|
.border_color(border)
|
|
.child(button)
|
|
.child(edge);
|
|
let cell = if column {
|
|
cell.top_0()
|
|
.left(px(start))
|
|
.w(px(size))
|
|
.h_full()
|
|
.border_r_1()
|
|
} else {
|
|
cell.left_0()
|
|
.top(px(start))
|
|
.h(px(size))
|
|
.w_full()
|
|
.border_b_1()
|
|
};
|
|
cell.into_any_element()
|
|
}
|
|
|
|
/// The square at the selection's corner; dragging it fills.
|
|
fn fill_handle(&self, view: &Entity<LokView>, cx: &mut Context<Self>) -> Option<AnyElement> {
|
|
let read = view.read(cx);
|
|
let area = span(read.selection()).or(read.cell().cursor)?;
|
|
let corner = read.place((area.0 + area.2, area.1 + area.3));
|
|
let start = read.place((area.0, area.1));
|
|
// Scrolled under the frozen part, the area shows nothing.
|
|
if corner.x <= start.x || corner.y <= start.y {
|
|
return None;
|
|
}
|
|
let theme = cx.theme();
|
|
let (fill, edge) = (theme.colors.focus, theme.colors.bg);
|
|
let preview = match &self.drag {
|
|
Some(Drag::Fill {
|
|
to: Some((column, row)),
|
|
}) => {
|
|
let geometry = &self.active_tab()?.extra.geometry;
|
|
let end = (
|
|
geometry.columns.start(column + 1),
|
|
geometry.rows.start(row + 1),
|
|
);
|
|
Some(outline(read.place((area.0, area.1)), read.place(end), fill))
|
|
}
|
|
_ => None,
|
|
};
|
|
let handle = div()
|
|
.id("table-fill")
|
|
.absolute()
|
|
.left(corner.x - px(HANDLE / 2.0))
|
|
.top(corner.y - px(HANDLE / 2.0))
|
|
.size(px(HANDLE))
|
|
.bg(fill)
|
|
.border_1()
|
|
.border_color(edge)
|
|
.cursor(CursorStyle::Crosshair)
|
|
.occlude()
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
cx.listener(|book, _: &MouseDownEvent, _, cx| {
|
|
cx.stop_propagation();
|
|
book.drag = Some(Drag::Fill { to: None });
|
|
cx.notify();
|
|
}),
|
|
);
|
|
let layer = div().absolute().top_0().left_0().size_full();
|
|
Some(layer.children(preview).child(handle).into_any_element())
|
|
}
|
|
|
|
fn on_drag_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
|
|
if !matches!(self.drag, Some(Drag::Fill { .. })) {
|
|
return;
|
|
}
|
|
let Some(tab) = self.active_tab() else {
|
|
return;
|
|
};
|
|
let read = tab.view.read(cx);
|
|
let (x, y) = read.twips_at(event.position - read.origin());
|
|
let geometry = &tab.extra.geometry;
|
|
let cell = (geometry.columns.at(x), geometry.rows.at(y));
|
|
self.drag = Some(Drag::Fill { to: Some(cell) });
|
|
cx.notify();
|
|
}
|
|
|
|
fn on_drag_end(&mut self, event: &MouseUpEvent, _: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(drag) = self.drag.take() else {
|
|
return;
|
|
};
|
|
cx.notify();
|
|
match drag {
|
|
Drag::Size { column, index, .. } => {
|
|
let Some(tab) = self.active_tab() else {
|
|
return;
|
|
};
|
|
let read = tab.view.read(cx);
|
|
let geometry = &tab.extra.geometry;
|
|
let axis = if column {
|
|
&geometry.columns
|
|
} else {
|
|
&geometry.rows
|
|
};
|
|
let local = event.position - read.origin();
|
|
let (x, y) = read.twips_at(local);
|
|
let twips = (if column { x } else { y }) - axis.start(index);
|
|
let size = mm100(twips).clamp(1, 65535).to_string();
|
|
let id = if column {
|
|
"columns.width"
|
|
} else {
|
|
"rows.height"
|
|
};
|
|
log::info!("table: {id} {} to {size}", index + 1);
|
|
self.run_active(id, &[&size, &(index + 1).to_string()], cx);
|
|
}
|
|
Drag::Fill {
|
|
to: Some((column, row)),
|
|
} => {
|
|
self.run_active("fill.auto", &[&address(column, row)], cx);
|
|
}
|
|
Drag::Fill { to: None } => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The rectangle that holds all of `rects`.
|
|
fn span(rects: &[Rect]) -> Option<Rect> {
|
|
let first = *rects.first()?;
|
|
let (mut x0, mut y0, mut x1, mut y1) = (first.0, first.1, first.0 + first.2, first.1 + first.3);
|
|
for &(x, y, w, h) in &rects[1..] {
|
|
(x0, y0) = (x0.min(x), y0.min(y));
|
|
(x1, y1) = (x1.max(x + w), y1.max(y + h));
|
|
}
|
|
Some((x0, y0, x1 - x0, y1 - y0))
|
|
}
|
|
|
|
/// A frame from `from` to `to`, view-local.
|
|
fn outline(from: Point<Pixels>, to: Point<Pixels>, color: gpui::Hsla) -> AnyElement {
|
|
div()
|
|
.absolute()
|
|
.left(from.x)
|
|
.top(from.y)
|
|
.w(to.x - from.x)
|
|
.h(to.y - from.y)
|
|
.border_2()
|
|
.border_color(color)
|
|
.into_any_element()
|
|
}
|