Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,10 @@ pub mod mock_time;

pub use model::get_milliseconds_since_epoch;
pub use model::Model;
pub use user_model::{BorderArea, ClipboardData, Diff, EventEmitter, Subscription, UserModel};
pub use user_model::{
common::{BorderArea, ClipboardData, ModelEvent},
event::{EventEmitter, Subscription},
history::Diff,
ui::SelectedView,
UserModel,
};
14 changes: 14 additions & 0 deletions base/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,20 @@ impl Model {
}
}

/// Returns a list of all cells that have been changed or are being evaluated
pub fn get_changed_cells(&self) -> Vec<crate::user_model::history::CellReference> {
self.cells
.keys()
.map(
|&(sheet, row, column)| crate::user_model::history::CellReference {
sheet,
row,
column,
},
)
.collect()
}

/// Removes the content of the cell but leaves the style.
///
/// See also:
Expand Down
85 changes: 52 additions & 33 deletions base/src/user_model/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use csv::{ReaderBuilder, WriterBuilder};
use serde::{Deserialize, Serialize};

use crate::{
constants::{self, LAST_COLUMN, LAST_ROW},
constants::{COLUMN_WIDTH_FACTOR, LAST_COLUMN, LAST_ROW},
expressions::{
types::{Area, CellReferenceIndex},
utils::{is_valid_column_number, is_valid_row},
Expand All @@ -16,15 +16,25 @@ use crate::{
Alignment, BorderItem, CellType, Col, HorizontalAlignment, SheetProperties, SheetState,
Style, VerticalAlignment,
},
user_model::{
event::{EventEmitter, Subscription},
history::{
CellReference, ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData,
},
},
utils::is_valid_hex_color,
};

use crate::user_model::history::{
ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData,
};

use super::border_utils::is_max_border;
use super::event::{EventEmitter, Subscription};

/// Events that the `UserModel` can emit.
pub enum ModelEvent {
/// A list of diffs (a single user action) that can be undone/redone.
Diffs(DiffList),
/// An informational event that cells have been evaluated.
CellsEvaluated(Vec<CellReference>),
}

/// Data for the clipboard
pub type ClipboardData = HashMap<i32, HashMap<i32, ClipboardCell>>;

Expand Down Expand Up @@ -224,7 +234,7 @@ pub struct UserModel {
history: History,
send_queue: Vec<QueueDiffs>,
pause_evaluation: bool,
event_emitter: EventEmitter<Diff>,
event_emitter: EventEmitter<ModelEvent>,
}

impl Debug for UserModel {
Expand Down Expand Up @@ -359,19 +369,19 @@ impl UserModel {
/// Subscribes to diff events.
/// Returns a Subscription handle that automatically unsubscribes when dropped.
#[cfg(any(target_arch = "wasm32", feature = "single_threaded"))]
pub fn subscribe<F>(&self, listener: F) -> Subscription<Diff>
pub fn subscribe<F>(&self, listener: F) -> Subscription<ModelEvent>
where
F: Fn(&Diff) + 'static,
F: Fn(&ModelEvent) + 'static,
{
self.event_emitter.subscribe(listener)
}

/// Subscribes to diff events.
/// Returns a Subscription handle that automatically unsubscribes when dropped.
#[cfg(not(any(target_arch = "wasm32", feature = "single_threaded")))]
pub fn subscribe<F>(&self, listener: F) -> Subscription<Diff>
pub fn subscribe<F>(&self, listener: F) -> Subscription<ModelEvent>
where
F: Fn(&Diff) + Send + Sync + 'static,
F: Fn(&ModelEvent) + Send + Sync + 'static,
{
self.event_emitter.subscribe(listener)
}
Expand All @@ -382,7 +392,22 @@ impl UserModel {
/// * [Model::evaluate]
/// * [UserModel::pause_evaluation]
pub fn evaluate(&mut self) {
self.model.evaluate()
// Perform evaluation
self.model.evaluate();

// Get the list of cells that were just evaluated
let evaluated_cells = self.model.get_changed_cells();

// Emit cells evaluated event if there are any cells that were evaluated
if !evaluated_cells.is_empty() {
self.event_emitter
.emit(&ModelEvent::CellsEvaluated(evaluated_cells));
}
}

/// Returns a list of all cells that have been changed or are being evaluated
pub fn get_changed_cells(&self) -> Vec<CellReference> {
self.model.get_changed_cells()
}

/// Returns the list of pending diffs and removes them from the queue
Expand Down Expand Up @@ -729,7 +754,7 @@ impl UserModel {
sheet,
row,
column,
old_style: Box::new(None),
old_style: Box::new(Some(old_style)),
});
}
}
Expand Down Expand Up @@ -759,7 +784,7 @@ impl UserModel {
sheet,
row: row.r,
column,
old_style: Box::new(None),
old_style: Box::new(Some(old_style)),
});
}
}
Expand Down Expand Up @@ -811,7 +836,7 @@ impl UserModel {
sheet,
row,
column,
old_style: Box::new(None),
old_style: Box::new(Some(old_style)),
});
}
}
Expand Down Expand Up @@ -866,7 +891,7 @@ impl UserModel {
sheet,
row,
column,
old_style: Box::new(None),
old_style: Box::new(Some(old_style)),
});
}
}
Expand Down Expand Up @@ -1705,13 +1730,13 @@ impl UserModel {
// remain in the copied area
let source = &CellReferenceIndex {
sheet,
column: *source_column,
row: *source_row,
column: *source_column,
};
let target = &CellReferenceIndex {
sheet,
column: target_column,
row: target_row,
column: target_column,
};
let new_value = if is_cut {
self.model
Expand Down Expand Up @@ -1897,15 +1922,12 @@ impl UserModel {
// **** Private methods ****** //

pub(crate) fn push_diff_list(&mut self, diff_list: DiffList) {
// Emit events for each diff before storing them
for diff in &diff_list {
self.event_emitter.emit(diff);
}

self.send_queue.push(QueueDiffs {
r#type: DiffType::Redo,
list: diff_list.clone(),
});
self.event_emitter
.emit(&ModelEvent::Diffs(diff_list.clone()));
self.history.push(diff_list);
}

Expand All @@ -1918,7 +1940,6 @@ impl UserModel {
fn apply_undo_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> {
let mut needs_evaluation = false;
for diff in diff_list.iter().rev() {
self.event_emitter.emit(diff);
match diff {
Diff::SetCellValue {
sheet,
Expand Down Expand Up @@ -2034,7 +2055,7 @@ impl UserModel {
}
// makes sure that the width and style is correct
if let Some(col) = &old_data.column {
let width = col.width * constants::COLUMN_WIDTH_FACTOR;
let width = col.width * COLUMN_WIDTH_FACTOR;
let style = col.style;
worksheet.set_column_width_and_style(*column, width, style)?;
}
Expand Down Expand Up @@ -2186,16 +2207,14 @@ impl UserModel {
Diff::DeleteRowStyle {
sheet,
row,
old_value,
old_value: _,
} => {
if let Some(s) = old_value.as_ref() {
self.model.set_row_style(*sheet, *row, s)?;
} else {
self.model.delete_row_style(*sheet, *row)?;
}
self.model.delete_row_style(*sheet, *row)?;
}
}
}
self.event_emitter
.emit(&ModelEvent::Diffs(diff_list.clone()));
if needs_evaluation {
self.evaluate_if_not_paused();
}
Expand All @@ -2206,7 +2225,6 @@ impl UserModel {
fn apply_diff_list(&mut self, diff_list: &DiffList) -> Result<(), String> {
let mut needs_evaluation = false;
for diff in diff_list {
self.event_emitter.emit(diff);
match diff {
Diff::SetCellValue {
sheet,
Expand Down Expand Up @@ -2398,7 +2416,8 @@ impl UserModel {
}
}
}

self.event_emitter
.emit(&ModelEvent::Diffs(diff_list.clone()));
if needs_evaluation {
self.evaluate_if_not_paused();
}
Expand Down
8 changes: 8 additions & 0 deletions base/src/user_model/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ pub struct ColumnData {
pub data: HashMap<i32, Cell>,
}

#[allow(missing_docs)]
#[derive(Clone, Encode, Decode, Serialize)]
pub struct CellReference {
pub sheet: u32,
pub row: i32,
pub column: i32,
}

#[allow(missing_docs)]
#[derive(Clone, Encode, Decode, Serialize)]
pub enum Diff {
Expand Down
11 changes: 4 additions & 7 deletions base/src/user_model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@

mod border;
mod border_utils;
mod common;
mod event;
mod history;
mod ui;
pub mod common;
pub mod event;
pub mod history;
pub mod ui;

pub use common::UserModel;
pub use event::{EventEmitter, Subscription};
pub use history::Diff;

#[cfg(test)]
pub use ui::SelectedView;

pub use common::BorderArea;
pub use common::ClipboardData;
7 changes: 7 additions & 0 deletions base/src/user_model/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ use super::common::UserModel;

#[derive(Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Debug))]
/// Represents the user's current view of the worksheet, including selection and scroll position.
pub struct SelectedView {
/// The index of the selected sheet.
pub sheet: u32,
/// The selected row.
pub row: i32,
/// The selected column.
pub column: i32,
/// The selected range, as `[start_row, start_column, end_row, end_column]`.
pub range: [i32; 4],
/// The first row visible in the viewport.
pub top_row: i32,
/// The first column visible in the viewport.
pub left_column: i32,
}

Expand Down
22 changes: 22 additions & 0 deletions bindings/wasm/README.pkg.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,25 @@ model.onDiffs(() => {

model.setUserInput(0, 1, 1, "=1+1");
```

To listen to cells that change during evaluation (formulas that recalculate):

```TypeScript
import init, { Model } from "@ironcalc/wasm";

await init();

const model = new Model("Sheet1", "en", "UTC");

model.onCellsEvaluated((cellReferences) => {
// cellReferences is an array of {sheet, row, column} objects
// that represent cells that were recalculated during evaluation
cellReferences.forEach(cell => {
console.log(`Cell ${cell.sheet}:${cell.row}:${cell.column} was evaluated`);
});
});

// Setting a formula will trigger evaluation
model.setUserInput(0, 1, 1, "=SUM(A2:A5)");
model.setUserInput(0, 2, 1, "10"); // This will trigger re-evaluation of A1
```
48 changes: 42 additions & 6 deletions bindings/wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,49 @@ impl Model {

#[wasm_bindgen(js_name = "onDiffs")]
pub fn on_diffs(&mut self, callback: Function) -> Function {
let subscription = self.model.subscribe(move |diff| {
match serde_wasm_bindgen::to_value(diff) {
Ok(js_diff) => {
let _ = callback.call1(&JsValue::NULL, &js_diff);
let subscription = self.model.subscribe(move |event| {
if let ironcalc_base::ModelEvent::Diffs(diffs) = event {
match serde_wasm_bindgen::to_value(diffs) {
Ok(js_diffs) => {
let _ = callback.call1(&JsValue::NULL, &js_diffs);
}
Err(_e) => {
// Silent skip: if serialization fails, we skip this diff event
}
}
Err(_e) => {
// Silent skip: if serialization fails, we skip this diff event
}
});

// Store subscription in an Rc<RefCell<>> so it can be moved into the closure
let subscription_rc = Rc::new(RefCell::new(Some(subscription)));
let subscription_clone = subscription_rc.clone();

// Create the unsubscribe function
let unsubscribe_fn = wasm_bindgen::closure::Closure::wrap(Box::new(move || {
if let Ok(mut sub) = subscription_clone.try_borrow_mut() {
if let Some(subscription) = sub.take() {
subscription.unsubscribe();
}
}
}) as Box<dyn FnMut()>);

let js_function = unsubscribe_fn.as_ref().unchecked_ref::<Function>().clone();
unsubscribe_fn.forget(); // Prevent the closure from being dropped

js_function
}

#[wasm_bindgen(js_name = "onCellsEvaluated")]
pub fn on_cells_evaluated(&mut self, callback: Function) -> Function {
let subscription = self.model.subscribe(move |event| {
if let ironcalc_base::ModelEvent::CellsEvaluated(cells) = event {
match serde_wasm_bindgen::to_value(cells) {
Ok(js_cells) => {
let _ = callback.call1(&JsValue::NULL, &js_cells);
}
Err(_e) => {
// Silent skip: if serialization fails, we skip this event
}
}
}
});
Expand Down
Loading