Support static pictures inside cells with XLSX roundtrip (#61)

This commit is contained in:
Harrison O'Connor-Hoover
2026-10-10 12:43:27 -04:00
parent c5bf34d5d7
commit 9dc3aaf9fa
39 changed files with 1659 additions and 86 deletions
+21 -7
View File
@@ -265,6 +265,7 @@ fn do_sort(s: &mut Session, r: RangeRef, header: bool, keys: Vec<SortKey>, by_co
// Move whole lines (cells with formulas adjusted for relative references).
let lanes: Vec<u32> = if by_cols { (body.start.row..=body.end.row).collect() } else { (body.start.col..=body.end.col).collect() };
let mut moved: Vec<(CellRef, Option<Cell>)> = Vec::with_capacity(body.count() as usize);
let mut pictures = Vec::new();
for (dst_i, &src_i) in order.iter().enumerate() {
for &k in &lanes {
let src = line(src_i, k);
@@ -277,6 +278,9 @@ fn do_sort(s: &mut Session, r: RangeRef, header: bool, keys: Vec<SortKey>, by_co
let shifted = gridcraft_formula::adjust::shift_relative(e, dst.row as i64 - src.row as i64, dst.col as i64 - src.col as i64);
c.formula = Some(Arc::new(Formula::from_expr(shifted)));
}
if let Some(picture) = sh.cell_pictures.get(&src) {
pictures.push((dst, picture.clone()));
}
moved.push((dst, cell));
}
}
@@ -286,12 +290,13 @@ fn do_sort(s: &mut Session, r: RangeRef, header: bool, keys: Vec<SortKey>, by_co
let shm = cx.sheet_mut(sheet)?;
for (c, cell) in &moved {
match cell {
Some(x) => shm.cells.set(*c, x.clone()),
Some(x) => shm.set_cell(*c, x.clone()),
None => {
shm.cells.remove(*c);
shm.remove_cell(*c);
}
}
}
shm.cell_pictures.extend(pictures);
for (i, h) in heights.into_iter().enumerate() {
let row = body.start.row + i as u32;
match h {
@@ -600,21 +605,30 @@ fn remove_duplicates(s: &mut Session, p: &Json) -> Result<Json> {
removed += 1;
}
}
let pictures: Vec<_> = keep
.iter()
.enumerate()
.flat_map(|(i, row)| {
(r.start.col..=r.end.col)
.filter_map(move |col| sh.cell_pictures.get(&CellRef::new(*row, col)).map(|p| (CellRef::new(start + i as u32, col), p.clone())))
})
.collect();
let rows: Vec<Vec<Option<Cell>>> =
keep.iter().map(|row| (r.start.col..=r.end.col).map(|c| sh.cell(CellRef::new(*row, c)).cloned()).collect()).collect();
let shm = cx.sheet_mut(sheet)?;
for row in start..=r.end.row {
for c in r.start.col..=r.end.col {
shm.cells.remove(CellRef::new(row, c));
shm.remove_cell(CellRef::new(row, c));
}
}
for (i, cells) in rows.into_iter().enumerate() {
for (j, cell) in cells.into_iter().enumerate() {
if let Some(cell) = cell {
shm.cells.set(CellRef::new(start + i as u32, r.start.col + j as u32), cell);
shm.set_cell(CellRef::new(start + i as u32, r.start.col + j as u32), cell);
}
}
}
shm.cell_pictures.extend(pictures);
for c in RangeRef::new(CellRef::new(start, r.start.col), r.end).iter() {
cx.changed.push((sheet, c));
}
@@ -665,9 +679,9 @@ fn text_to_columns(s: &mut Session, p: &Json) -> Result<Json> {
let cell = super::edit::input_to_cell(&t, sheet, c, &mut cx.wb).unwrap_or(None);
let shm = cx.sheet_mut(sheet)?;
match cell {
Some(x) => shm.cells.set(c, x),
Some(x) => shm.set_cell(c, x),
None => {
shm.cells.remove(c);
shm.remove_cell(c);
}
}
cx.changed.push((sheet, c));
@@ -873,7 +887,7 @@ fn circle_invalid(s: &mut Session, _: &Json) -> Result<Json> {
let r = sh.used_range().and_then(|u| r.intersection(&u));
let Some(r) = r else { continue };
for c in r.iter().take(100_000) {
let text = sh.cell(c).map(|x| x.input_text()).unwrap_or_default();
let text = sh.input_text(c);
if !text.is_empty() && check_validation(&d.wb, sheet, c, &text).is_some() {
bad_cells.push(c.a1());
}
+34 -17
View File
@@ -213,16 +213,16 @@ fn cell_set(s: &mut Session, p: &Json) -> Result<Json> {
std::sync::Arc::make_mut(f).array = Some(range);
}
let sh = cx.sheet_mut(sheet)?;
sh.cells.set(range.start, cell);
sh.set_cell(range.start, cell);
cx.touch(sheet, range.start);
return Ok(Json::Null);
}
let cell = input_to_cell(&input, sheet, at, &mut cx.wb)?;
let sh = cx.sheet_mut(sheet)?;
match cell {
Some(c) => sh.cells.set(at, c),
Some(c) => sh.set_cell(at, c),
None => {
sh.cells.remove(at);
sh.remove_cell(at);
}
}
cx.touch(sheet, at);
@@ -278,9 +278,9 @@ fn range_set_values(s: &mut Session, p: &Json) -> Result<Json> {
};
let sh = cx.sheet_mut(sheet)?;
match cell {
Some(c) => sh.cells.set(at, c),
Some(c) => sh.set_cell(at, c),
None => {
sh.cells.remove(at);
sh.remove_cell(at);
}
}
cx.touch(sheet, at);
@@ -312,7 +312,7 @@ fn range_fill(s: &mut Session, p: &Json) -> Result<Json> {
cell.formula = Some(std::sync::Arc::new(Formula::from_expr(shifted)));
}
cell.style = cx.wb.sheet(sheet).and_then(|sh| sh.cell(at)).map(|c| c.style).unwrap_or(cell.style);
cx.sheet_mut(sheet)?.cells.set(at, cell);
cx.sheet_mut(sheet)?.set_cell(at, cell);
cx.touch(sheet, at);
}
}
@@ -668,9 +668,9 @@ fn paste_text(s: &mut Session, p: &Json, text: &str) -> Result<Json> {
let cell = input_to_cell(v, sheet, c, &mut cx.wb).unwrap_or_else(|_| Some(Cell::value(Value::text(v.as_str()))));
let sh = cx.sheet_mut(sheet)?;
match cell {
Some(cell) => sh.cells.set(c, cell),
Some(cell) => sh.set_cell(c, cell),
None => {
sh.cells.remove(c);
sh.remove_cell(c);
}
}
cx.touch(sheet, c);
@@ -783,9 +783,12 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
*style_map.entry(id).or_insert_with(|| wb.styles.intern(src_wb.styles.get(id).clone()))
};
let mut moved_formulas = Vec::new();
let mut moved_pictures = std::collections::BTreeMap::new();
if cut_move {
// Remove the source first (contents and formats), and fix references to it.
let taken = cx.sheet_mut(clip.sheet)?.cells.take_range(src);
let source = cx.sheet_mut(clip.sheet)?;
moved_pictures = source.cell_pictures.iter().filter(|(c, _)| src.contains(**c)).map(|(c, p)| (*c, p.clone())).collect();
let taken = source.take_cells(src);
for (c, _) in &taken {
cx.touch(clip.sheet, *c);
}
@@ -804,10 +807,13 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
src_sheet.cell(sc).cloned()
};
let src_val = src_sheet.value(sc);
if skip_blanks && src_val.is_empty() && src_cell.as_ref().is_none_or(|x| x.formula.is_none()) {
let src_picture = if cut_move { moved_pictures.get(&sc) } else { src_sheet.cell_pictures.get(&sc) }.cloned();
if skip_blanks && src_picture.is_none() && src_val.is_empty() && src_cell.as_ref().is_none_or(|x| x.formula.is_none()) {
continue;
}
let old = cx.wb.sheet(dest_sheet).and_then(|sh| sh.cell(dest)).cloned().unwrap_or_default();
let old_picture = cx.wb.sheet(dest_sheet).and_then(|sh| sh.cell_pictures.get(&dest)).cloned();
let mut new_picture = old_picture.clone();
let mut new = old.clone();
let shift = |cell: &Cell| -> Option<std::sync::Arc<Formula>> {
let f = cell.formula.as_ref()?;
@@ -827,10 +833,12 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
};
new.formula = Some(std::sync::Arc::new(Formula::new(&format!("={sheet_prefix}{}", sc.a1()))));
new.value = Value::Empty;
new_picture = None;
} else {
let sc_cell = src_cell.clone().unwrap_or_default();
match what {
What::All | What::AllExceptBorders => {
new_picture = src_picture.clone();
new.formula = shift(&sc_cell);
new.value = if new.formula.is_some() { Value::Empty } else { sc_cell.value.clone() };
let st = map_style(sc_cell.style, &mut cx.wb);
@@ -842,6 +850,7 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
};
}
What::Formulas | What::FormulasAndNumberFormats => {
new_picture = src_picture.clone();
new.formula = shift(&sc_cell);
new.value = if new.formula.is_some() { Value::Empty } else { sc_cell.value.clone() };
if what == What::FormulasAndNumberFormats {
@@ -850,6 +859,7 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
}
}
What::Values | What::ValuesAndNumberFormats => {
new_picture = src_picture.clone();
new.formula = None;
new.value = src_val.clone();
if what == What::ValuesAndNumberFormats {
@@ -865,6 +875,10 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
&& matches!(what, What::All | What::Values | What::ValuesAndNumberFormats | What::Formulas)
&& new.formula.is_none()
{
if old_picture.is_some() || new_picture.is_some() {
new_picture = None;
new.value = Value::Error(gridcraft_core::CellError::Value);
}
let a = old.value.clone();
let b = new.value.clone();
if let (Ok(x), Ok(y)) = (a.to_number(), b.to_number())
@@ -902,9 +916,12 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
if let Some(cm) = src_sheet.comments.get(&sc).cloned() {
cx.sheet_mut(dest_sheet)?.comments.insert(dest, cm);
}
cx.sheet_mut(dest_sheet)?.cells.set(dest, new);
cx.sheet_mut(dest_sheet)?.set_cell(dest, new);
}
_ => cx.sheet_mut(dest_sheet)?.cells.set(dest, new),
_ => cx.sheet_mut(dest_sheet)?.set_cell(dest, new),
}
if let Some(picture) = new_picture {
cx.sheet_mut(dest_sheet)?.cell_pictures.insert(dest, picture);
}
cx.touch(dest_sheet, dest);
}
@@ -997,12 +1014,12 @@ fn clear(s: &mut Session, p: &Json, what: &str) -> Result<Json> {
for (c, mut cell) in cells {
match what {
"all" => {
sh.cells.remove(c);
sh.remove_cell(c);
}
"contents" => {
cell.value = Value::Empty;
cell.formula = None;
sh.cells.set(c, cell);
sh.set_cell(c, cell);
}
"formats" => {
cell.style = StyleId::DEFAULT;
@@ -1222,7 +1239,7 @@ fn replace(s: &mut Session, p: &Json) -> Result<Json> {
let mut n = 0;
for si in sheets {
let cells: Vec<(CellRef, String)> =
cx.wb.sheet(si).map(|sh| sh.cells.iter().map(|(c, cell)| (c, cell.input_text())).collect()).unwrap_or_default();
cx.wb.sheet(si).map(|sh| sh.cells.iter().map(|(c, _)| (c, sh.input_text(c))).collect()).unwrap_or_default();
for (c, text) in cells {
if !all && c != active {
continue;
@@ -1240,9 +1257,9 @@ fn replace(s: &mut Session, p: &Json) -> Result<Json> {
let cell = input_to_cell(&new, si, c, &mut cx.wb).unwrap_or_else(|_| Some(Cell::value(Value::text(new.as_str()))));
let sh = cx.sheet_mut(si)?;
match cell {
Some(cell) => sh.cells.set(c, cell),
Some(cell) => sh.set_cell(c, cell),
None => {
sh.cells.remove(c);
sh.remove_cell(c);
}
}
cx.touch(si, c);
+1 -1
View File
@@ -840,7 +840,7 @@ fn checkbox(s: &mut Session, p: &Json) -> Result<Json> {
let sh = cx.sheet_mut(si)?;
let v = sh.value(c);
let value = if matches!(v, Value::Bool(_)) { v } else { Value::Bool(false) };
sh.cells.set(c, Cell { value, formula: None, style: new });
sh.set_cell(c, Cell { value, formula: None, style: new });
cx.touch(si, c);
}
}
+7
View File
@@ -56,6 +56,13 @@ fn open(s: &mut Session, p: &Json) -> Result<Json> {
s.close_document(0);
}
let i = s.add_document(DocState::new(wb, path, title));
if !warnings.is_empty() {
let mut message = warnings.iter().take(5).cloned().collect::<Vec<_>>().join("\n\n");
if warnings.len() > 5 {
message.push_str(&format!("\n\n{} additional import warnings.", warnings.len() - 5));
}
s.ui_requests.push(crate::UiRequest::Message(message));
}
let d = s.doc()?;
Ok(
json!({"index": i, "title": d.display_title(), "sheets": d.wb.sheets.iter().map(|s| s.name.clone()).collect::<Vec<_>>(), "warnings": warnings}),
+1 -1
View File
@@ -585,7 +585,7 @@ fn merge(s: &mut Session, p: &Json, how: &str) -> Result<Json> {
if let Some(mut cell) = sh.cells.get(c).cloned() {
cell.value = gridcraft_core::Value::Empty;
cell.formula = None;
sh.cells.set(c, cell);
sh.set_cell(c, cell);
}
touched.push((sheet, c));
}
+65 -7
View File
@@ -96,7 +96,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Picture",
["Insert", "Illustrations"],
None,
"{path? | base64?: \"...\", mime?, at?: \"B2\", width?, height?, alt?}",
"{path? | base64?: \"...\", mime?, at?: \"B2\", width?, height?, alt?, placement?: cell|overCells}",
has_doc,
insert_picture
),
@@ -220,6 +220,13 @@ fn insert_table(s: &mut Session, p: &Json) -> Result<Json> {
} else {
let sh = cx.sheet_mut(sheet)?;
sh.cells.shift_rows_in_cols(r.start.col, r.end.col, r.start.row, 1);
sh.cell_pictures = std::mem::take(&mut sh.cell_pictures)
.into_iter()
.filter_map(|(c, v)| {
let dest = if c.col >= r.start.col && c.col <= r.end.col && c.row >= r.start.row { c.offset(1, 0) } else { Some(c) };
dest.map(|c| (c, v))
})
.collect();
for (i, c) in (r.start.col..=r.end.col).enumerate() {
sh.set_value(CellRef::new(r.start.row, c), gridcraft_core::Value::text(format!("Column{}", i + 1)));
}
@@ -304,7 +311,7 @@ fn table_flag(s: &mut Session, p: &Json, which: &str) -> Result<Json> {
t.range.end.row = t.range.end.row.saturating_sub(1);
let (c0, c1) = (t.range.start.col, t.range.end.col);
for c in c0..=c1 {
sh.cells.remove(CellRef::new(row, c));
sh.remove_cell(CellRef::new(row, c));
}
}
write_totals(sh, ti);
@@ -333,12 +340,11 @@ fn write_totals(sh: &mut Sheet, ti: usize) {
let c = CellRef::new(row, t.range.start.col + i as u32);
if let Some(code) = col.totals.subtotal_code() {
let f = format!("=SUBTOTAL({code},{}[{}])", t.name, col.name);
sh.cells
.set(c, Cell { formula: Some(std::sync::Arc::new(Formula::new(&f))), value: gridcraft_core::Value::Empty, style: sh.style_id(c) });
sh.set_cell(c, Cell { formula: Some(std::sync::Arc::new(Formula::new(&f))), value: gridcraft_core::Value::Empty, style: sh.style_id(c) });
} else if let Some(l) = &col.totals_label {
sh.set_value(c, gridcraft_core::Value::text(l.as_str()));
} else {
sh.cells.remove(c);
sh.remove_cell(c);
}
}
}
@@ -696,14 +702,39 @@ fn insert_sparkline(s: &mut Session, p: &Json) -> Result<Json> {
}
fn insert_picture(s: &mut Session, p: &Json) -> Result<Json> {
let placement = str_param(p, "placement").unwrap_or("overCells");
if !matches!(placement, "cell" | "overCells") {
return Err(bad("insert.picture", "placement must be cell or overCells"));
}
let data = if let Some(b) = str_param(p, "base64") {
crate::io::base64_decode(b).ok_or_else(|| bad("insert.picture", "invalid base64"))?
} else if let Some(path) = str_param(p, "path") {
crate::io::read_file(path)?
} else {
s.ui_requests.push(crate::UiRequest::Dialog("insertPicture".into(), json!({})));
s.ui_requests.push(crate::UiRequest::Dialog("insertPicture".into(), json!({"placement": placement})));
return ok();
};
if placement == "cell" {
let mime = validate_cell_picture(&data)?;
let d = s.doc()?;
let sheet = d.wb.active_sheet;
let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
let at = match str_param(p, "at") {
Some(a) => CellRef::parse(a).ok_or_else(|| bad("insert.picture", "invalid cell address"))?,
None => d.selection.active,
};
let at = sh.merge_at(at).map(|r| r.start).unwrap_or(at);
let style = sh.style_id(at);
if sh.is_protected() && d.wb.styles.get(style).protection.locked {
return Err(EngineError::Other("The cell you're trying to change is on a protected sheet.".into()));
}
let picture = CellPicture { data, mime: mime.into(), alt: str_param(p, "alt").unwrap_or("").into() };
return edit(s, |cx| {
cx.sheet_mut(sheet)?.set_picture(at, std::sync::Arc::new(picture));
cx.touch(sheet, at);
Ok(json!({"cell": at.a1(), "placement": "cell"}))
});
}
if data.len() > 64 * 1024 * 1024 {
return Err(bad("insert.picture", "image larger than 64 MB"));
}
@@ -735,6 +766,33 @@ fn insert_picture(s: &mut Session, p: &Json) -> Result<Json> {
})
}
/// Validate before mutating the workbook; header dimensions alone do not prove the pixels decode.
fn validate_cell_picture(data: &[u8]) -> Result<&'static str> {
if data.len() > 16 * 1024 * 1024 {
return Err(bad("insert.picture", "cell pictures must be at most 16 MiB"));
}
let format = image::guess_format(data).map_err(|_| bad("insert.picture", "choose a valid PNG or JPEG picture"))?;
let mime = match format {
image::ImageFormat::Png => "image/png",
image::ImageFormat::Jpeg => "image/jpeg",
_ => return Err(bad("insert.picture", "cell pictures support PNG and JPEG")),
};
let (width, height) = image::ImageReader::with_format(std::io::Cursor::new(data), format)
.into_dimensions()
.map_err(|_| bad("insert.picture", "invalid image dimensions"))?;
if width == 0 || height == 0 || width > 8192 || height > 8192 || u64::from(width) * u64::from(height) > 16_000_000 {
return Err(bad("insert.picture", "cell pictures are limited to 8192 pixels per side and 16 million pixels"));
}
let mut reader = image::ImageReader::with_format(std::io::Cursor::new(data), format);
let mut limits = image::Limits::default();
limits.max_image_width = Some(8192);
limits.max_image_height = Some(8192);
limits.max_alloc = Some(64 * 1024 * 1024);
reader.limits(limits);
reader.decode().map_err(|_| bad("insert.picture", "picture cannot be decoded within the 64 MiB limit"))?;
Ok(mime)
}
fn insert_shape(s: &mut Session, p: &Json) -> Result<Json> {
let kind = match str_param(p, "kind").unwrap_or("rectangle") {
"roundedRectangle" => ShapeKind::RoundedRectangle,
@@ -846,7 +904,7 @@ fn insert_link(s: &mut Session, p: &Json) -> Result<Json> {
});
let sh = cx.sheet_mut(sheet)?;
if let Some(t) = text.clone().or_else(|| sh.value(at).is_empty().then(|| target.clone())) {
sh.cells.set(at, Cell { value: gridcraft_core::Value::text(t.as_str()), formula: None, style: link_style });
sh.set_cell(at, Cell { value: gridcraft_core::Value::text(t.as_str()), formula: None, style: link_style });
} else {
sh.set_style(at, link_style);
}
+3 -2
View File
@@ -125,10 +125,11 @@ fn cell_get(s: &mut Session, p: &Json) -> Result<Json> {
Ok(json!({
"cell": c.a1(),
"value": value_json(&sh.value(c)),
"type": match sh.value(c) { Value::Empty => "empty", Value::Number(_) => "number", Value::Text(_) => "text", Value::Bool(_) => "boolean", Value::Error(_) => "error", Value::Array(_) => "array" },
"type": if sh.cell_pictures.contains_key(&c) { "picture" } else { match sh.value(c) { Value::Empty => "empty", Value::Number(_) => "number", Value::Text(_) => "text", Value::Bool(_) => "boolean", Value::Error(_) => "error", Value::Array(_) => "array" } },
"formula": cell.and_then(|x| x.formula.as_ref()).map(|f| format!("={}", f.text)),
"text": crate::display::cell_text(&d.wb, sh, c),
"input": cell.map(|x| x.input_text()).unwrap_or_default(),
"input": sh.input_text(c),
"picture": sh.cell_pictures.get(&c).map(|picture| json!({"mime": picture.mime, "alt": picture.alt, "bytes": picture.data.len()})),
"spilledFrom": sh.spill_ranges.iter().find(|(a, r)| **a != c && r.contains(c)).map(|(a, _)| a.a1()),
"spill": sh.spill_ranges.get(&c).map(|r| r.a1()),
"style": style,
+31 -1
View File
@@ -847,11 +847,41 @@ fn draw_block(pg: &mut Page, b: &Block, gridlines: bool, cf: &mut Option<crate::
if w <= 0.0 || h <= 0.0 {
continue;
}
draw_text(pg, b, *c, l, (x, y, w, h), merged, &merges, (bx, bx + bw));
if let Some(picture) = sh.cell_pictures.get(c) {
draw_cell_picture(pg, &picture.data, (x, y, w, h), b.k);
} else {
draw_text(pg, b, *c, l, (x, y, w, h), merged, &merges, (bx, bx + bw));
}
}
pg.restore();
}
/// Cell pictures fit the current cell (or merged-cell) rectangle without changing aspect ratio.
fn draw_cell_picture(pg: &mut Page, data: &[u8], rect: (f32, f32, f32, f32), k: f32) {
let (x, y, w, h) = rect;
let pad = 2.0 * k;
let (w, h) = ((w - 2.0 * pad).max(0.0), (h - 2.0 * pad).max(0.0));
if w <= 0.0 || h <= 0.0 || data.len() > 16 * 1024 * 1024 {
return;
}
let Some((iw, ih)) = crate::io::image_size(data) else { return };
if iw == 0 || ih == 0 || iw > 8192 || ih > 8192 || u64::from(iw) * u64::from(ih) > 16_000_000 {
return;
}
let Ok(mut reader) = image::ImageReader::new(std::io::Cursor::new(data)).with_guessed_format() else { return };
let mut limits = image::Limits::default();
limits.max_image_width = Some(8192);
limits.max_image_height = Some(8192);
limits.max_alloc = Some(64 * 1024 * 1024);
reader.limits(limits);
let Ok(image) = reader.decode() else { return };
let image =
image.thumbnail(((w / 72.0) * 150.0).ceil().clamp(1.0, 4000.0) as u32, ((h / 72.0) * 150.0).ceil().clamp(1.0, 4000.0) as u32).to_rgba8();
let fit = (w / iw as f32).min(h / ih as f32);
let (pw, ph) = (iw as f32 * fit, ih as f32 * fit);
pg.image_rgba(x + pad + (w - pw) / 2.0, y + pad + (h - ph) / 2.0, pw, ph, image.as_raw(), image.width(), image.height());
}
/// Is a cell free for text to overflow into?
fn empty_for_overflow(sh: &Sheet, c: CellRef, merges: &[RangeRef]) -> bool {
sh.value_ref(c).is_none_or(|v| v.is_empty()) && !merges.iter().any(|m| m.contains(c))
+6 -2
View File
@@ -137,6 +137,8 @@ fn shift_sheet_features(sh: &mut Sheet, axis: Axis, at: u32, count: u32, insert:
None => sh.autofilter = None,
}
}
sh.cell_pictures =
std::mem::take(&mut sh.cell_pictures).into_iter().filter_map(|(c, v)| map_cell(c).filter(CellRef::is_valid).map(|c| (c, v))).collect();
sh.comments = std::mem::take(&mut sh.comments).into_iter().filter_map(|(c, v)| map_cell(c).map(|c| (c, v))).collect();
sh.hyperlinks = std::mem::take(&mut sh.hyperlinks).into_iter().filter_map(|(c, v)| map_cell(c).map(|c| (c, v))).collect();
for ch in sh.charts.iter_mut() {
@@ -247,7 +249,7 @@ fn delete_lines(s: &mut Session, p: &Json, axis: Axis) -> Result<Json> {
Axis::Rows => RangeRef::rows(at, (at + count - 1).min(MAX_ROWS - 1)),
Axis::Cols => RangeRef::cols(at, (at + count - 1).min(MAX_COLS - 1)),
};
sh.cells.take_range(block);
sh.take_cells(block);
match axis {
Axis::Rows => sh.cells.shift_rows(at + count, -(count as i64)),
Axis::Cols => sh.cells.shift_cols(at + count, -(count as i64)),
@@ -305,7 +307,7 @@ fn delete_cells(s: &mut Session, p: &Json) -> Result<Json> {
let name = cx.wb.sheet(sheet).map(|s| s.name.clone()).unwrap_or_default();
{
let sh = cx.sheet_mut(sheet)?;
sh.cells.take_range(r);
sh.take_cells(r);
if left {
sh.cells.shift_cols_in_rows(r.start.row, r.end.row, r.end.col + 1, -(r.width() as i64));
} else {
@@ -648,6 +650,8 @@ fn shift_cell_features(sh: &mut Sheet, e: &Edit) {
None => sh.autofilter = None,
}
}
sh.cell_pictures =
std::mem::take(&mut sh.cell_pictures).into_iter().filter_map(|(c, v)| map_cell(c).filter(CellRef::is_valid).map(|c| (c, v))).collect();
sh.comments = std::mem::take(&mut sh.comments).into_iter().filter_map(|(c, v)| map_cell(c).map(|c| (c, v))).collect();
sh.hyperlinks = std::mem::take(&mut sh.hyperlinks).into_iter().filter_map(|(c, v)| map_cell(c).map(|c| (c, v))).collect();
for sp in sh.sparklines.iter_mut() {
+10 -10
View File
@@ -201,7 +201,7 @@ impl Trial {
for (si, c, v) in sets {
if let Some(sh) = wb.sheet_mut(*si) {
let style = sh.style_id(*c);
sh.cells.set(*c, Cell { value: v.clone(), formula: None, style });
sh.set_cell(*c, Cell { value: v.clone(), formula: None, style });
keys.push((*si, *c));
}
}
@@ -330,7 +330,7 @@ fn goal_seek(s: &mut Session, p: &Json) -> Result<Json> {
edit(s, |cx| {
let sh = cx.sheet_mut(csi)?;
let style = sh.style_id(chg);
sh.cells.set(chg, Cell { value: Value::number(r.x), formula: None, style });
sh.set_cell(chg, Cell { value: Value::number(r.x), formula: None, style });
cx.touch(csi, chg);
Ok(())
})?;
@@ -492,9 +492,9 @@ fn scenario_show(s: &mut Session, p: &Json) -> Result<Json> {
let cell = super::edit::input_to_cell(&input, si, *c, &mut cx.wb)?;
let sh = cx.sheet_mut(si)?;
match cell {
Some(cell) => sh.cells.set(*c, cell),
Some(cell) => sh.set_cell(*c, cell),
None => {
sh.cells.remove(*c);
sh.remove_cell(*c);
}
}
cx.touch(si, *c);
@@ -571,7 +571,7 @@ fn scenario_summary(s: &mut Session, p: &Json) -> Result<Json> {
let bold = cx.wb.styles.derive(StyleId::DEFAULT, |st| st.font.bold = true);
let mut sheet = Sheet::new(name.clone());
let put = |sheet: &mut Sheet, row: u32, col: u32, v: Value, style: StyleId| {
sheet.cells.set(CellRef::new(row, col), Cell { value: v, formula: None, style });
sheet.set_cell(CellRef::new(row, col), Cell { value: v, formula: None, style });
};
put(&mut sheet, 1, 1, Value::text("Scenario Summary"), bold);
put(&mut sheet, 2, 2, Value::text("Current Values:"), bold);
@@ -694,7 +694,7 @@ fn data_table(s: &mut Session, p: &Json) -> Result<Json> {
for (c, v) in out {
let sh = cx.sheet_mut(si)?;
let style = sh.style_id(c);
sh.cells.set(c, Cell { value: v, formula: None, style });
sh.set_cell(c, Cell { value: v, formula: None, style });
cx.touch(si, c);
}
Ok(())
@@ -890,10 +890,10 @@ fn advanced_filter(s: &mut Session, p: &Json) -> Result<Json> {
let shm = cx.sheet_mut(dsi)?;
let old: Vec<CellRef> = shm.cells.iter_range(extract).map(|(c, _)| c).collect();
for c in &old {
shm.cells.remove(*c);
shm.remove_cell(*c);
}
for (c, cell) in cells {
shm.cells.set(c, cell);
shm.set_cell(c, cell);
}
for c in old {
cx.touch(dsi, c);
@@ -1107,9 +1107,9 @@ fn consolidate(s: &mut Session, p: &Json) -> Result<Json> {
let sh = cx.sheet_mut(dsi)?;
let style = sh.style_id(c);
if v.is_empty() && style == StyleId::DEFAULT {
sh.cells.remove(c);
sh.remove_cell(c);
} else {
sh.cells.set(c, Cell { value: v, formula: None, style });
sh.set_cell(c, Cell { value: v, formula: None, style });
}
cx.touch(dsi, c);
}