mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-25 19:26:50 -06:00
ROADMAP.md: all progress-log rows kept. docs/ui-parity.md: one Last updated line, change notes combined; both history rows kept; ribbon row wording tidied.
This commit is contained in:
@@ -134,6 +134,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|---|---|
|
||||
| 2026-10-13 | Formula-entry Command/Ctrl-click inserts another reference with the locale's argument separator; Shift-click extends from a stable, sheet-aware anchor. Pointer tests cover repeated clicks, reversed ranges, ordinary replacement, typed separators, Spanish syntax, sheet switches and keyboard continuation. Readiness estimates unchanged. |
|
||||
| 2026-10-13 | `formulas.evaluate` (`gridcraft-cli eval`, MCP `evaluate_formula`) refuses what entering the formula in a cell refuses, with the reason (`=SUM(1;2)`: "expected `,` or `)`"), instead of `#NAME?` for any formula that doesn't parse |
|
||||
| 2026-10-13 | Colour menus: Fill Color, Font Color and the Format Cells pickers end with Recent Colors (the last ten custom colours) and More Colors… (hue/saturation area, RGB, hex box, preview, OK/Cancel) (#928) |
|
||||
| 2026-10-12 | Rows auto-fit oversized fonts: opening a file and whole-row/column formatting grow non-custom rows to their tallest font (#833) |
|
||||
| 2026-10-12 | Number formats: parsing a format code is linear in its length (long runs of `[`, `,` or `/` no longer take quadratic time), and an unclosed `[` is a literal when splitting sections as well as when tokenizing (#810) |
|
||||
| 2026-10-12 | Number formats: a fraction denominator with 65,536 zeros no longer crashes; format codes are capped at 255 characters like Excel (longer codes display as General, `TEXT` gives `#VALUE!`) (#807) |
|
||||
@@ -195,6 +196,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | trivial | Progress log: More Colors… and Recent Colors in the colour menus (#928) |
|
||||
| 2026-10-12 | minor | Rows auto-fit oversized fonts on open and whole-row/column formatting (#833); progress log row |
|
||||
| 2026-10-12 | minor | Progress log: multi-area row/column delete (#758) |
|
||||
| 2026-10-12 | trivial | Progress log: XLSX chart sheets kept on save (#727) |
|
||||
|
||||
@@ -241,7 +241,7 @@ fn format_chart(app: &mut SheetApp, ui: &mut Ui) {
|
||||
ui.label(&name);
|
||||
ui.label(egui::RichText::new(&s.values).small().color(Color32::from_gray(110)));
|
||||
ui.menu_button("Fill…", |ui| {
|
||||
if let Some(c) = crate::widgets::color_palette(ui, &theme.colors, "Automatic") {
|
||||
if let Some(c) = crate::widgets::color_palette_fixed(ui, &theme.colors, "Automatic") {
|
||||
app.run_or_alert(
|
||||
"chart.formatSelection",
|
||||
json!({"chart": id, "series": i, "color": if c == "none" { Json::Null } else { json!(c) }}),
|
||||
|
||||
@@ -676,15 +676,18 @@ fn home(app: &mut SheetApp, ui: &mut Ui) {
|
||||
act(app, "home.fillColor", json!({"color": fill}));
|
||||
}
|
||||
let forced = invoke(app, "home.fillColor");
|
||||
egui::Popup::menu(&fa).open_memory(popup_command(forced, fa.clicked())).show(|ui| {
|
||||
if let Some(c) = color_palette(ui, &theme_colors(app), "No Fill") {
|
||||
if c != "none" {
|
||||
app.grid.last_fill = c.clone();
|
||||
egui::Popup::menu(&fa)
|
||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
||||
.open_memory(popup_command(forced, fa.clicked()))
|
||||
.show(|ui| {
|
||||
if let Some(c) = color_palette(ui, &theme_colors(app), "No Fill") {
|
||||
if c != "none" {
|
||||
app.grid.last_fill = c.clone();
|
||||
}
|
||||
act(app, "home.fillColor", json!({"color": c}));
|
||||
ui.close();
|
||||
}
|
||||
act(app, "home.fillColor", json!({"color": c}));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
let fc = app.grid.last_font_color.clone();
|
||||
let fc_c = gridcraft_engine::model::Color::from_hex(&fc).and_then(|c| c.resolve(&Default::default())).map(theme::color32);
|
||||
let (c, ca) = split_button(ui, Icon::FontColor, fc_c, "Font Color");
|
||||
@@ -693,15 +696,18 @@ fn home(app: &mut SheetApp, ui: &mut Ui) {
|
||||
act(app, "home.fontColor", json!({"color": fc}));
|
||||
}
|
||||
let forced = invoke(app, "home.fontColor");
|
||||
egui::Popup::menu(&ca).open_memory(popup_command(forced, ca.clicked())).show(|ui| {
|
||||
if let Some(c) = color_palette(ui, &theme_colors(app), "Automatic") {
|
||||
if c != "none" {
|
||||
app.grid.last_font_color = c.clone();
|
||||
egui::Popup::menu(&ca)
|
||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
||||
.open_memory(popup_command(forced, ca.clicked()))
|
||||
.show(|ui| {
|
||||
if let Some(c) = color_palette(ui, &theme_colors(app), "Automatic") {
|
||||
if c != "none" {
|
||||
app.grid.last_font_color = c.clone();
|
||||
}
|
||||
act(app, "home.fontColor", json!({"color": if c == "none" { "auto".to_string() } else { c }}));
|
||||
ui.close();
|
||||
}
|
||||
act(app, "home.fontColor", json!({"color": if c == "none" { "auto".to_string() } else { c }}));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
sep(ui);
|
||||
|
||||
@@ -209,7 +209,7 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) {
|
||||
}
|
||||
ui.menu_button("Tab Color", |ui| {
|
||||
let colors = app.session.active().map(|d| d.wb.theme.colors).unwrap_or_default();
|
||||
if let Some(c) = crate::widgets::color_palette(ui, &colors, "No Color") {
|
||||
if let Some(c) = crate::widgets::color_palette_fixed(ui, &colors, "No Color") {
|
||||
app.run_or_alert("sheet.tabColor", json!({"sheet": i, "color": c}));
|
||||
ui.close();
|
||||
}
|
||||
|
||||
+169
-10
@@ -144,23 +144,84 @@ pub fn committed_text(ui: &mut Ui, id: egui::Id, current: &str, width: Option<f3
|
||||
None
|
||||
}
|
||||
|
||||
/// Our colour palette (theme row + tints + standard colours), returns a picked hex colour,
|
||||
/// `Some("none")` for No Fill/Automatic. Arrow keys move a highlight through the swatches and
|
||||
/// Enter applies; the key events are stripped so the grid behind the popup does not also react.
|
||||
/// How many custom colours the palette's Recent Colors row remembers.
|
||||
pub const RECENT_COLORS: usize = 10;
|
||||
|
||||
/// `#RRGGBB` for a colour.
|
||||
pub fn hex_of(rgb: [u8; 3]) -> String {
|
||||
format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2])
|
||||
}
|
||||
|
||||
/// A colour typed as `#RRGGBB`, `RRGGBB`, `#RGB` or `RGB` (any case, spaces around it ignored).
|
||||
pub fn parse_hex(text: &str) -> Option<[u8; 3]> {
|
||||
let t = text.trim();
|
||||
let t = t.strip_prefix('#').unwrap_or(t);
|
||||
let digits: Vec<u32> = t.chars().map(|c| c.to_digit(16)).collect::<Option<_>>()?;
|
||||
let byte = |hi: u32, lo: u32| u8::try_from(hi * 16 + lo).ok();
|
||||
match digits[..] {
|
||||
[r, g, b] => Some([byte(r, r)?, byte(g, g)?, byte(b, b)?]),
|
||||
[r1, r2, g1, g2, b1, b2] => Some([byte(r1, r2)?, byte(g1, g2)?, byte(b1, b2)?]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts `hex` first in the recent colours, once, keeping at most [`RECENT_COLORS`].
|
||||
pub fn remember_color(recent: &mut Vec<String>, hex: &str) {
|
||||
let hex = hex.to_ascii_uppercase();
|
||||
recent.retain(|h| *h != hex);
|
||||
recent.insert(0, hex);
|
||||
recent.truncate(RECENT_COLORS);
|
||||
}
|
||||
|
||||
fn recent_colors_id() -> egui::Id {
|
||||
egui::Id::new("gridcraft.recent_colors")
|
||||
}
|
||||
|
||||
/// The More Colors… picker's state while it is open in a palette.
|
||||
#[derive(Clone)]
|
||||
struct MoreColors {
|
||||
hsva: egui::ecolor::Hsva,
|
||||
hex: String,
|
||||
}
|
||||
|
||||
impl MoreColors {
|
||||
fn new(rgb: [u8; 3]) -> Self {
|
||||
Self { hsva: egui::ecolor::Hsva::from_srgb(rgb), hex: hex_of(rgb) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Our colour palette (theme row + tints + standard colours, then Recent Colors and More Colors…),
|
||||
/// returns a picked hex colour, `Some("none")` for No Fill/Automatic. Arrow keys move a highlight
|
||||
/// through the swatches and Enter applies; the key events are stripped so the grid behind the popup
|
||||
/// does not also react (not while More Colors… is open, whose boxes take the keys).
|
||||
pub fn color_palette(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &str) -> Option<String> {
|
||||
palette(ui, theme_colors, none_label, true)
|
||||
}
|
||||
|
||||
/// [`color_palette`] without More Colors…, for a menu that closes on any click inside it (a sub-menu
|
||||
/// of a context menu), where the picker would vanish as soon as it is used. Recent Colors stays.
|
||||
pub fn color_palette_fixed(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &str) -> Option<String> {
|
||||
palette(ui, theme_colors, none_label, false)
|
||||
}
|
||||
|
||||
fn palette(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &str, more_colors: bool) -> Option<String> {
|
||||
let mut picked = None;
|
||||
const ROWS: usize = 7; // 6 tint rows + 1 standard row
|
||||
const COLS: usize = 10;
|
||||
let pal_id = ui.id().with("__color_palette");
|
||||
let mut cur: Option<(usize, usize)> = ui.data_mut(|d| d.get_temp(pal_id)).unwrap_or(None);
|
||||
let more_id = pal_id.with("more");
|
||||
let mut more: Option<MoreColors> = ui.data_mut(|d| d.get_temp(more_id)).unwrap_or(None);
|
||||
let (mut dl, mut dr, mut du, mut dd, mut ent) = (false, false, false, false, false);
|
||||
ui.input(|i| {
|
||||
dl = i.key_pressed(egui::Key::ArrowLeft);
|
||||
dr = i.key_pressed(egui::Key::ArrowRight);
|
||||
du = i.key_pressed(egui::Key::ArrowUp);
|
||||
dd = i.key_pressed(egui::Key::ArrowDown);
|
||||
ent = i.key_pressed(egui::Key::Enter);
|
||||
});
|
||||
if more.is_none() {
|
||||
ui.input(|i| {
|
||||
dl = i.key_pressed(egui::Key::ArrowLeft);
|
||||
dr = i.key_pressed(egui::Key::ArrowRight);
|
||||
du = i.key_pressed(egui::Key::ArrowUp);
|
||||
dd = i.key_pressed(egui::Key::ArrowDown);
|
||||
ent = i.key_pressed(egui::Key::Enter);
|
||||
});
|
||||
}
|
||||
if dl || dr || du || dd {
|
||||
cur = match cur {
|
||||
None => Some((
|
||||
@@ -266,6 +327,73 @@ pub fn color_palette(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &str) ->
|
||||
}
|
||||
}
|
||||
});
|
||||
// Recent Colors: the custom colours picked below, newest first.
|
||||
let mut recent: Vec<String> = ui.ctx().data(|d| d.get_temp(recent_colors_id())).unwrap_or_default();
|
||||
if !recent.is_empty() {
|
||||
ui.label(egui::RichText::new("Recent Colors").small());
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 2.0;
|
||||
for hex in &recent {
|
||||
let c = parse_hex(hex).map_or(Color32::BLACK, |c| Color32::from_rgb(c[0], c[1], c[2]));
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(16.0, 14.0), Sense::click());
|
||||
ui.painter().rect_filled(r, 1.0, c);
|
||||
ui.painter().rect_stroke(r, 1.0, Stroke::new(1.0, Color32::from_gray(200)), StrokeKind::Inside);
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_stroke(r.expand(1.0), 1.0, Stroke::new(1.5, Color32::from_rgb(0xE8, 0x7D, 0x2B)), StrokeKind::Outside);
|
||||
}
|
||||
if resp.on_hover_text(hex).clicked() {
|
||||
picked = Some(hex.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if more_colors {
|
||||
ui.separator();
|
||||
}
|
||||
if more_colors && ui.button("More Colors…").clicked() {
|
||||
let start = recent.first().and_then(|h| parse_hex(h)).unwrap_or([0x44, 0x72, 0xC4]);
|
||||
more = Some(MoreColors::new(start));
|
||||
}
|
||||
if let Some(m) = more.as_mut() {
|
||||
let mut done = false;
|
||||
// The saturation / value area with its hue bar and egui's red-green-blue boxes, then the hex
|
||||
// box with the new colour beside it.
|
||||
if egui::color_picker::color_picker_hsva_2d(ui, &mut m.hsva, egui::color_picker::Alpha::Opaque) {
|
||||
m.hex = hex_of(m.hsva.to_srgb());
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label("Hex");
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut m.hex).desired_width(72.0).char_limit(7)).labelled_by(label.id);
|
||||
if r.changed()
|
||||
&& let Some(c) = parse_hex(&m.hex)
|
||||
{
|
||||
m.hsva = egui::ecolor::Hsva::from_srgb(c);
|
||||
}
|
||||
let c = m.hsva.to_srgb();
|
||||
let (sw, _) = ui.allocate_exact_size(vec2(36.0, 18.0), Sense::hover());
|
||||
ui.painter().rect_filled(sw, 1.0, Color32::from_rgb(c[0], c[1], c[2]));
|
||||
ui.painter().rect_stroke(sw, 1.0, Stroke::new(1.0, Color32::from_gray(160)), StrokeKind::Inside);
|
||||
ui.label(egui::RichText::new("New").small());
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button(" OK ").clicked() {
|
||||
// A hex still being typed that is not a colour falls back to the picker's.
|
||||
let c = parse_hex(&m.hex).unwrap_or_else(|| m.hsva.to_srgb());
|
||||
let hex = hex_of(c);
|
||||
remember_color(&mut recent, &hex);
|
||||
picked = Some(hex);
|
||||
done = true;
|
||||
}
|
||||
if ui.button("Cancel").clicked() {
|
||||
done = true;
|
||||
}
|
||||
});
|
||||
if done {
|
||||
more = None;
|
||||
}
|
||||
}
|
||||
ui.ctx().data_mut(|d| d.insert_temp(recent_colors_id(), recent));
|
||||
ui.data_mut(|d| d.insert_temp(more_id, more));
|
||||
ui.data_mut(|d| d.insert_temp(pal_id, cur));
|
||||
picked
|
||||
}
|
||||
@@ -362,3 +490,34 @@ pub fn notice_bar(app: &mut SheetApp, ui: &mut Ui) {
|
||||
app.notice = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod color_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hex_is_read_in_the_forms_people_type() {
|
||||
assert_eq!(parse_hex("#1A2b3C"), Some([0x1A, 0x2B, 0x3C]));
|
||||
assert_eq!(parse_hex(" 1a2b3c "), Some([0x1A, 0x2B, 0x3C]));
|
||||
assert_eq!(parse_hex("#f80"), Some([0xFF, 0x88, 0x00]));
|
||||
for bad in ["", "#", "#12", "#1234", "#12345", "#1234567", "#12345G", "##123456", "é12345"] {
|
||||
assert_eq!(parse_hex(bad), None, "{bad:?}");
|
||||
}
|
||||
assert_eq!(hex_of([0, 10, 255]), "#000AFF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_colours_keep_the_newest_first_once_each_and_at_most_ten() {
|
||||
let mut r = Vec::new();
|
||||
for i in 0..12u8 {
|
||||
remember_color(&mut r, &hex_of([i, 0, 0]));
|
||||
}
|
||||
assert_eq!(r.len(), RECENT_COLORS);
|
||||
assert_eq!(r[0], "#0B0000");
|
||||
remember_color(&mut r, "#050000");
|
||||
remember_color(&mut r, "#0b0000");
|
||||
assert_eq!(&r[..2], ["#0B0000", "#050000"]);
|
||||
assert_eq!(r.iter().filter(|h| *h == "#050000").count(), 1);
|
||||
assert_eq!(r.len(), RECENT_COLORS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//! The colour menus' More Colors… and Recent Colors, through the real egui input path.
|
||||
|
||||
use egui::{Event, Key, Modifiers, os::OperatingSystem};
|
||||
use egui_kittest::kittest::Queryable as _;
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_ui_egui::SheetApp;
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||
|
||||
fn harness() -> Harness {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
let mut h = egui_kittest::Harness::builder().with_os(OperatingSystem::Nix).with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(s, Default::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn press(h: &mut Harness, key: Key, modifiers: Modifiers) {
|
||||
let ev = |pressed| Event::Key { key, physical_key: None, pressed, repeat: false, modifiers };
|
||||
h.input_mut().events.extend([ev(true), ev(false)]);
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// H, H opens the Fill Color palette.
|
||||
fn open_fill_palette(h: &mut Harness) {
|
||||
for k in [Key::F10, Key::H, Key::H] {
|
||||
press(h, k, Modifiers::NONE);
|
||||
}
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn type_hex(h: &mut Harness, hex: &str) {
|
||||
h.get_by_label("Hex").click();
|
||||
h.run_steps(2);
|
||||
press(h, Key::A, Modifiers::COMMAND);
|
||||
h.input_mut().events.push(Event::Text(hex.to_string()));
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_colors_applies_the_typed_colour_and_remembers_it() {
|
||||
let mut h = harness();
|
||||
open_fill_palette(&mut h);
|
||||
assert!(h.query_by_label("Recent Colors").is_none(), "nothing is recent before a custom colour is used");
|
||||
h.get_by_label("More Colors…").click();
|
||||
h.run_steps(3);
|
||||
type_hex(&mut h, "#12AB34");
|
||||
h.get_by_label(" OK ").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().grid.last_fill, "#12AB34");
|
||||
assert_eq!(h.state().session.journal.iter().filter(|(id, _)| id == "home.fillColor").count(), 1);
|
||||
assert!(h.query_by_label("Theme Colors").is_none(), "the palette closes after OK");
|
||||
// The next time any colour menu opens, the colour is the first Recent Color.
|
||||
open_fill_palette(&mut h);
|
||||
h.get_by_label("Recent Colors");
|
||||
assert!(h.query_by_label("Hex").is_none(), "More Colors… starts closed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_colour_that_is_not_hex_does_not_apply_and_cancel_applies_nothing() {
|
||||
let mut h = harness();
|
||||
open_fill_palette(&mut h);
|
||||
h.get_by_label("More Colors…").click();
|
||||
h.run_steps(3);
|
||||
// Enter in the Hex box does not apply the palette's highlighted swatch or close the menu.
|
||||
type_hex(&mut h, "#12AB");
|
||||
press(&mut h, Key::Enter, Modifiers::NONE);
|
||||
assert!(h.state().session.journal.iter().all(|(id, _)| id != "home.fillColor"));
|
||||
h.get_by_label("Hex");
|
||||
h.get_by_label("Cancel").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().session.journal.iter().all(|(id, _)| id != "home.fillColor"));
|
||||
assert!(h.query_by_label("Hex").is_none());
|
||||
assert!(h.query_by_label("Recent Colors").is_none(), "Cancel remembers nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_font_colour_menu_has_them_too_and_shares_the_recent_colours() {
|
||||
let mut h = harness();
|
||||
open_fill_palette(&mut h);
|
||||
h.get_by_label("More Colors…").click();
|
||||
h.run_steps(3);
|
||||
type_hex(&mut h, "#00FF7F");
|
||||
h.get_by_label(" OK ").click();
|
||||
h.run_steps(4);
|
||||
for k in [Key::F10, Key::H, Key::F, Key::C] {
|
||||
press(&mut h, k, Modifiers::NONE);
|
||||
}
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Automatic");
|
||||
h.get_by_label("Recent Colors");
|
||||
h.get_by_label("More Colors…");
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; Ctrl/Option-drag a sheet tab drops a copy, #971; formula-entry Command/Ctrl-click adds arguments and Shift-click extends a range) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; Ctrl/Option-drag a sheet tab drops a copy, #971; formula-entry Command/Ctrl-click adds arguments and Shift-click extends a range; More Colors… and Recent Colors, #928) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
||||
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
||||
@@ -16,7 +16,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
||||
|---|---|---|---|
|
||||
| 2026-10-13 | minor | Dragging a sheet tab with Ctrl held (Cmd or Option on macOS) drops a copy of the sheet where the tab is released, with the copy cursor (#971) |
|
||||
| Title bar, Quick Access Toolbar, AutoSave toggle | yes | yes | done; macOS window controls misaligned (#170), Dock icon size (#169) |
|
||||
| Ribbon: tabs, groups, split buttons, galleries, contextual tabs | yes | yes (Table Design, Chart Design, Format, PivotTable, Header & Footer tabs) | done; missing contextual tabs for pictures, shapes, sparklines, slicers |
|
||||
| Ribbon: tabs, groups, split buttons, galleries, contextual tabs | yes | yes (Table Design, Chart Design, Format, PivotTable, Header & Footer tabs); Fill Color, Font Color and the Format Cells colour pickers end with Recent Colors and More Colors… (#928) | done; missing contextual tabs for pictures, shapes, sparklines, slicers |
|
||||
| Ribbon collapse, simplified ribbon, customize ribbon/QAT | yes | partial | customization missing (File ▸ Options is the one missing File command) |
|
||||
| Ribbon: tabs, groups, split buttons, galleries, contextual tabs | yes | yes (Table Design, Chart Design, Format, PivotTable tabs) | done; missing contextual tabs for pictures, shapes, sparklines, header/footer, slicers |
|
||||
| Ribbon collapse, simplified ribbon, customize ribbon/QAT | yes | partial | customization missing |
|
||||
@@ -164,6 +164,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
||||
|---|---|---|
|
||||
| 2026-10-19 | minor | Ctrl+Enter (`range.fill`) gives each cell what typing the input there gives: a General cell gets the automatic number format (`5%`, `$5`, `3/4/2026`, `=TODAY()`) even when it already held a value, and a cell with its own format, a formatted row or column, or Text keeps it. Cells that held a value used to keep General (`5%` showed 0.05), and empty cells took the active cell's format |
|
||||
| 2026-10-18 | minor | Keytip letters typed with left Alt held continue the sequence (Alt+H, O, A / Alt+H, O, I ran nothing before: Alt+letter cancelled keytips); other Alt combinations, AltGr and macOS Option are unchanged (#1846) |
|
||||
| 2026-10-13 | trivial | More Colors… and Recent Colors (#928) |
|
||||
| 2026-10-12 | minor | The File view's recent list tells when each workbook was last changed, read off the UI thread |
|
||||
| 2026-10-13 | minor | Horizontal scrollbar in the sheet tab bar: drag, track click pages, frozen columns excluded from the page (#840) |
|
||||
| 2026-10-13 | minor | Formula-entry Command/Ctrl-click adds arguments and Shift-click extends references; ordinary replacement and dragging retain their behavior. Readiness estimates unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user