Skip to content

Commit 8b80b3d

Browse files
committed
feat: show selection stats in footer
1 parent c9369c7 commit 8b80b3d

2 files changed

Lines changed: 108 additions & 4 deletions

File tree

src/screen/editor/context.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use crate::{
2-
model::{cell::CellAddress, workbook::Workbook},
2+
model::{
3+
cell::{CellAddress, CellValue},
4+
workbook::Workbook,
5+
},
36
screen::ScreenCommand,
47
theme::Theme,
58
};
@@ -22,6 +25,14 @@ pub struct TableContext {
2225
pending_command: Option<ScreenCommand>,
2326
}
2427

28+
/// 当前选区的统计信息。
29+
#[derive(Debug, Clone, Copy, PartialEq)]
30+
pub struct SelectionStats {
31+
pub average: f64,
32+
pub sum: f64,
33+
pub count: usize,
34+
}
35+
2536
impl TableContext {
2637
/// 创建编辑器共享状态。
2738
pub fn new(theme: Theme, path: String, wb: Workbook) -> Self {
@@ -72,6 +83,52 @@ impl TableContext {
7283
self.selection = None;
7384
}
7485

86+
/// 统计当前多单元格矩形选区。
87+
///
88+
/// `count` 统计有内容的单元格;`sum` 和 `average` 只统计数字。
89+
pub fn selection_stats(&self) -> Option<SelectionStats> {
90+
let selection = self.selection?;
91+
let Selection::Range { .. } = selection else {
92+
return None;
93+
};
94+
95+
let (r1, r2, c1, c2) = selection.normalized_bounds(self.wb.rows, self.wb.columns);
96+
if r1 == r2 && c1 == c2 {
97+
return None;
98+
}
99+
100+
let mut sum = 0.0;
101+
let mut number_count = 0;
102+
let mut value_count = 0;
103+
104+
for row in r1..=r2 {
105+
for col in c1..=c2 {
106+
let addr = CellAddress { row, col };
107+
let Some(cell) = self.wb.get_cell(addr) else {
108+
continue;
109+
};
110+
111+
value_count += 1;
112+
if let Some(number) = cell_value_as_number(&cell.value) {
113+
sum += number;
114+
number_count += 1;
115+
}
116+
}
117+
}
118+
119+
let average = if number_count == 0 {
120+
0.0
121+
} else {
122+
sum / number_count as f64
123+
};
124+
125+
Some(SelectionStats {
126+
average,
127+
sum,
128+
count: value_count,
129+
})
130+
}
131+
75132
/// 已复制的区域(用于渲染虚线边框)。
76133
///
77134
/// `None` 表示没有待粘贴的复制内容。
@@ -261,3 +318,11 @@ impl TableContext {
261318
.unwrap_or_default()
262319
}
263320
}
321+
322+
fn cell_value_as_number(value: &CellValue) -> Option<f64> {
323+
match value {
324+
CellValue::Number(n) => Some(*n),
325+
CellValue::Text(text) => text.parse::<f64>().ok(),
326+
CellValue::Empty | CellValue::Error(_) => None,
327+
}
328+
}

src/screen/editor/navigation.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,28 @@ impl Mode for NavigationMode {
145145

146146
fn footer(&self, ctx: &TableContext) -> FooterLine {
147147
use ratatui::text::Span;
148-
FooterLine {
149-
hint: Some(Line::from(vec![
148+
let hint = if let Some(stats) = ctx.selection_stats() {
149+
Line::from(vec![
150+
Span::styled("平均值=", Style::default().fg(ctx.theme.accent)),
151+
Span::styled(
152+
format!(" {}", format_number(stats.average)),
153+
Style::default().fg(ctx.theme.text_dim),
154+
),
155+
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
156+
Span::styled("求和=", Style::default().fg(ctx.theme.accent)),
157+
Span::styled(
158+
format!(" {}", format_number(stats.sum)),
159+
Style::default().fg(ctx.theme.text_dim),
160+
),
161+
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
162+
Span::styled("计数=", Style::default().fg(ctx.theme.accent)),
163+
Span::styled(
164+
format!(" {}", stats.count),
165+
Style::default().fg(ctx.theme.text_dim),
166+
),
167+
])
168+
} else {
169+
Line::from(vec![
150170
Span::styled("Ctrl+S", Style::default().fg(ctx.theme.accent)),
151171
Span::styled(" 保存", Style::default().fg(ctx.theme.text_dim)),
152172
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
@@ -155,7 +175,11 @@ impl Mode for NavigationMode {
155175
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
156176
Span::styled("Enter", Style::default().fg(ctx.theme.accent)),
157177
Span::styled(" 编辑", Style::default().fg(ctx.theme.text_dim)),
158-
])),
178+
])
179+
};
180+
181+
FooterLine {
182+
hint: Some(hint),
159183
status: Some(Line::from(vec![
160184
Span::styled("[", Style::default().fg(ctx.theme.text_dim)),
161185
Span::styled(
@@ -208,3 +232,18 @@ impl NavigationMode {
208232
}
209233
}
210234
}
235+
236+
fn format_number(n: f64) -> String {
237+
if n == 0.0 {
238+
return "0".to_string();
239+
}
240+
241+
let abs = n.abs();
242+
if abs < 1e-6 || abs >= 1e12 {
243+
return format!("{:.2e}", n);
244+
}
245+
246+
let text = format!("{:.10}", n);
247+
let text = text.trim_end_matches('0');
248+
text.trim_end_matches('.').to_string()
249+
}

0 commit comments

Comments
 (0)