R5.5: ElyTable right-click menus, Excel-style; three rounds used, last fixes tested

Grid and header menus share ely_office::menu with ElyDoc. Each item's
availability is the kernel's state for the exact command it runs
(patch 0045). Patch 0046 lets a header select its row or column while
a shape is selected or its text is edited: the view shell ends the
drawing edit, keeps the typed text, leaves drawing mode, then marks.
Ely no longer sends Esc first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-14 13:02:10 -04:00
1 parent 07c0b43542
commit 87886c8a8d
30 files changed
+1010 -106

No files matched your search

+1
View File
@@ -9,6 +9,7 @@ pub mod engine;
pub mod files;
mod find;
pub mod lok;
pub mod menu;
mod open;
mod recover;
pub mod panel;
+86
View File
@@ -0,0 +1,86 @@
//! Ely's right-click menus, gated by the kernel's (DOC-02).
use std::io;
use ely_gpui_component::{
menus::{ContextMenu, Menu, MenuItem},
primitives::IconName,
};
use ely_lok::{LokView, menu::Offered};
use gpui::{App, Entity, Pixels, Point};
/// One menu, asked for at a point in one tab.
pub struct Offer<W> {
pub tab: u64,
pub what: W,
/// Bumps for each menu, so each opens once.
request: (u64, Point<Pixels>),
}
impl<W> Offer<W> {
/// The next menu after `last`.
pub fn new(last: Option<&Offer<W>>, tab: u64, what: W, at: Point<Pixels>) -> Self {
let number = last.map_or(1, |last| last.request.0 + 1);
Offer { tab, what, request: (number, at) }
}
/// Its host, opened at the point.
pub fn host(&self, id: &'static str, menu: Menu) -> ContextMenu {
ContextMenu::new(id, menu).manual(Some(self.request))
}
}
/// The view's last kernel menu and where it opens.
pub fn take(view: &Entity<LokView>, cx: &mut App) -> Option<io::Result<(Offered, Point<Pixels>)>> {
let (json, at) = view.update(cx, |view, _| view.take_menu())?;
let offered = match ely_lok::menu::read(&json) {
Ok(offered) => offered,
Err(error) => return Some(Err(error)),
};
// Without a click it opens at the caret.
let caret = || view.read(cx).cursor_bounds().map(|caret| caret.bottom_left());
let Some(at) = at.or_else(caret) else {
log::warn!("office: a menu with no click and no caret");
return None;
};
log::info!("office: a {:?} menu", offered.target);
Some(Ok((offered, at)))
}
/// A menu row: its label key and its gating command.
pub trait Pick: Copy + 'static {
fn row(self) -> (&'static str, &'static str);
}
/// A row, a submenu or a line.
pub enum Entry<P: 'static> {
Row(P),
Sub(&'static str, &'static [P]),
Line,
}
/// Cut, copy and paste as one row of icons.
pub fn clipboard<P: Pick>(menu: Menu, picks: [P; 3], item: impl Fn(P) -> MenuItem) -> Menu {
let icons = [IconName::Scissors, IconName::Copy, IconName::Clipboard];
let row = picks.into_iter().zip(icons).map(|(pick, icon)| item(pick).icon(icon));
menu.buttons(row)
}
/// Adds entries; a submenu is on when any row is.
pub fn entries<P: Pick>(
menu: Menu,
entries: &[Entry<P>],
cx: &App,
on: impl Fn(P) -> bool,
item: impl Fn(P) -> MenuItem,
) -> Menu {
entries.iter().fold(menu, |menu, entry| match entry {
Entry::Row(pick) => menu.item(item(*pick)),
Entry::Sub(key, picks) => {
let sub = picks.iter().fold(Menu::new(), |sub, pick| sub.item(item(*pick)));
let any = picks.iter().any(|pick| on(*pick));
menu.item(MenuItem::submenu(crate::text(cx, key, &[]), sub).disabled(!any))
}
Entry::Line => menu.separator(),
})
}