Skip to content

Commit be36d5a

Browse files
committed
refactor: split TableContext into EditorState and WorkbookController
Decompose the 430-line TableContext monolith into EditorState (viewport, selection, status-bar messages) and WorkbookController (cell operations, paste, save). EditorView becomes a lightweight Selection snapshot. Mode render/footer receive EditorReadModel instead of &TableContext.
1 parent 7fe1b00 commit be36d5a

13 files changed

Lines changed: 601 additions & 580 deletions

File tree

src/screen/editor/context.rs

Lines changed: 0 additions & 430 deletions
This file was deleted.

src/screen/editor/delete.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ use ratatui::{
1010
use crate::screen::EventResult;
1111
use crate::widget::selectable_list::{SelectableItem, SelectableList};
1212

13-
use super::{
14-
context::TableContext,
15-
mode::{EditorIntent, EditorView, FooterLine, Mode, ModeKind, ModeResult},
13+
use super::mode::{
14+
EditorIntent, EditorReadModel, EditorView, FooterLine, Mode, ModeKind, ModeResult,
1615
};
1716

1817
const PANEL_WIDTH: u16 = 20;
@@ -68,32 +67,32 @@ impl Mode for DeleteMode {
6867
}
6968
}
7069

71-
fn render(&self, frame: &mut Frame, area: Rect, ctx: &TableContext) -> Rect {
70+
fn render(&self, frame: &mut Frame, area: Rect, read: EditorReadModel<'_>) -> Rect {
7271
let [table_area, panel_area] =
7372
Layout::horizontal([Constraint::Fill(1), Constraint::Length(PANEL_WIDTH)]).areas(area);
7473

7574
let panel_block = Block::default()
76-
.style(Style::default().bg(ctx.theme.surface_alt))
77-
.title(Line::styled("删除", Style::default().fg(ctx.theme.accent)));
75+
.style(Style::default().bg(read.theme.surface_alt))
76+
.title(Line::styled("删除", Style::default().fg(read.theme.accent)));
7877
let inner = panel_block.inner(panel_area);
7978
frame.render_widget(panel_block, panel_area);
80-
self.list.render(frame, inner, ctx.theme);
79+
self.list.render(frame, inner, read.theme);
8180

8281
table_area
8382
}
8483

85-
fn footer(&self, ctx: &TableContext) -> FooterLine {
84+
fn footer(&self, read: EditorReadModel<'_>) -> FooterLine {
8685
use ratatui::text::Span;
8786
FooterLine {
8887
hint: Some(Line::from(vec![
89-
Span::styled("↑ / ↓", Style::default().fg(ctx.theme.accent)),
90-
Span::styled(" 选择", Style::default().fg(ctx.theme.text_dim)),
91-
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
92-
Span::styled("Enter", Style::default().fg(ctx.theme.accent)),
93-
Span::styled(" 确认", Style::default().fg(ctx.theme.text_dim)),
94-
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
95-
Span::styled("Esc", Style::default().fg(ctx.theme.accent)),
96-
Span::styled(" 取消", Style::default().fg(ctx.theme.text_dim)),
88+
Span::styled("↑ / ↓", Style::default().fg(read.theme.accent)),
89+
Span::styled(" 选择", Style::default().fg(read.theme.text_dim)),
90+
Span::styled(" ", Style::default().fg(read.theme.text_dim)),
91+
Span::styled("Enter", Style::default().fg(read.theme.accent)),
92+
Span::styled(" 确认", Style::default().fg(read.theme.text_dim)),
93+
Span::styled(" ", Style::default().fg(read.theme.text_dim)),
94+
Span::styled("Esc", Style::default().fg(read.theme.accent)),
95+
Span::styled(" 取消", Style::default().fg(read.theme.text_dim)),
9796
])),
9897
status: None,
9998
}

src/screen/editor/edit.rs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ use ratatui::{
1010
use crate::screen::EventResult;
1111

1212
use super::{
13-
context::TableContext,
14-
mode::{EditorIntent, EditorView, FooterLine, Mode, ModeKind, ModeResult},
13+
mode::{EditorIntent, EditorReadModel, EditorView, FooterLine, Mode, ModeKind, ModeResult},
1514
navigation::NavigationMode,
1615
};
1716

@@ -53,30 +52,30 @@ impl Mode for EditMode {
5352
}
5453
}
5554

56-
fn render(&self, frame: &mut Frame, area: Rect, ctx: &TableContext) -> Rect {
55+
fn render(&self, frame: &mut Frame, area: Rect, read: EditorReadModel<'_>) -> Rect {
5756
use ratatui::text::Span;
5857

5958
let [table_area, edit_area] =
6059
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
6160

6261
let mut spans = vec![Span::styled(
6362
"编辑: ",
64-
Style::default().fg(ctx.theme.accent),
63+
Style::default().fg(read.theme.accent),
6564
)];
6665
if self.buffer.is_empty() {
6766
spans.push(Span::styled(
6867
"(空)",
69-
Style::default().fg(ctx.theme.text_dim),
68+
Style::default().fg(read.theme.text_dim),
7069
));
7170
} else {
7271
spans.push(Span::styled(
7372
self.buffer.as_str(),
74-
Style::default().fg(ctx.theme.text),
73+
Style::default().fg(read.theme.text),
7574
));
76-
let cursor_char = if ctx.blink_visible() { "█" } else { " " };
75+
let cursor_char = if read.blink_visible { "█" } else { " " };
7776
spans.push(Span::styled(
7877
cursor_char,
79-
Style::default().fg(ctx.theme.text),
78+
Style::default().fg(read.theme.text),
8079
));
8180
}
8281

@@ -88,24 +87,24 @@ impl Mode for EditMode {
8887
Some(&self.buffer)
8988
}
9089

91-
fn footer(&self, ctx: &TableContext) -> FooterLine {
90+
fn footer(&self, read: EditorReadModel<'_>) -> FooterLine {
9291
use ratatui::text::Span;
9392
FooterLine {
9493
hint: Some(Line::from(vec![
95-
Span::styled("Enter", Style::default().fg(ctx.theme.accent)),
96-
Span::styled(" 确认", Style::default().fg(ctx.theme.text_dim)),
97-
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
98-
Span::styled("Esc", Style::default().fg(ctx.theme.accent)),
99-
Span::styled(" 取消", Style::default().fg(ctx.theme.text_dim)),
94+
Span::styled("Enter", Style::default().fg(read.theme.accent)),
95+
Span::styled(" 确认", Style::default().fg(read.theme.text_dim)),
96+
Span::styled(" ", Style::default().fg(read.theme.text_dim)),
97+
Span::styled("Esc", Style::default().fg(read.theme.accent)),
98+
Span::styled(" 取消", Style::default().fg(read.theme.text_dim)),
10099
])),
101100
status: Some(Line::from(vec![
102-
Span::styled("[", Style::default().fg(ctx.theme.text_dim)),
101+
Span::styled("[", Style::default().fg(read.theme.text_dim)),
103102
Span::styled(
104-
ctx.cursor().display(),
105-
Style::default().fg(ctx.theme.accent),
103+
read.viewport.cursor().display(),
104+
Style::default().fg(read.theme.accent),
106105
),
107-
Span::styled(", 编辑模式", Style::default().fg(ctx.theme.text_dim)),
108-
Span::styled("]", Style::default().fg(ctx.theme.text_dim)),
106+
Span::styled(", 编辑模式", Style::default().fg(read.theme.text_dim)),
107+
Span::styled("]", Style::default().fg(read.theme.text_dim)),
109108
])),
110109
}
111110
}

src/screen/editor/menu.rs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ use ratatui::{
1010
use crate::screen::EventResult;
1111
use crate::widget::selectable_list::{SelectableItem, SelectableList};
1212

13-
use super::{
14-
context::TableContext,
15-
mode::{EditorIntent, EditorView, FooterLine, Mode, ModeKind, ModeResult},
13+
use super::mode::{
14+
EditorIntent, EditorReadModel, EditorView, FooterLine, Mode, ModeKind, ModeResult,
1615
};
1716

1817
const MENU_WIDTH: u16 = 20;
@@ -64,30 +63,30 @@ impl Mode for MenuMode {
6463
}
6564
}
6665

67-
fn render(&self, frame: &mut Frame, area: Rect, ctx: &TableContext) -> Rect {
66+
fn render(&self, frame: &mut Frame, area: Rect, read: EditorReadModel<'_>) -> Rect {
6867
let [table_area, menu_area] =
6968
Layout::horizontal([Constraint::Fill(1), Constraint::Length(MENU_WIDTH)]).areas(area);
7069

71-
let menu_block = Block::default().style(Style::default().bg(ctx.theme.surface_alt));
70+
let menu_block = Block::default().style(Style::default().bg(read.theme.surface_alt));
7271
let inner = menu_block.inner(menu_area);
7372
frame.render_widget(menu_block, menu_area);
74-
self.list.render(frame, inner, ctx.theme);
73+
self.list.render(frame, inner, read.theme);
7574

7675
table_area
7776
}
7877

79-
fn footer(&self, ctx: &TableContext) -> FooterLine {
78+
fn footer(&self, read: EditorReadModel<'_>) -> FooterLine {
8079
use ratatui::text::Span;
8180
FooterLine {
8281
hint: Some(Line::from(vec![
83-
Span::styled("↑ / ↓", Style::default().fg(ctx.theme.accent)),
84-
Span::styled(" 选择", Style::default().fg(ctx.theme.text_dim)),
85-
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
86-
Span::styled("Enter", Style::default().fg(ctx.theme.accent)),
87-
Span::styled(" 确认", Style::default().fg(ctx.theme.text_dim)),
88-
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
89-
Span::styled("Ctrl+P", Style::default().fg(ctx.theme.accent)),
90-
Span::styled(" 关闭", Style::default().fg(ctx.theme.text_dim)),
82+
Span::styled("↑ / ↓", Style::default().fg(read.theme.accent)),
83+
Span::styled(" 选择", Style::default().fg(read.theme.text_dim)),
84+
Span::styled(" ", Style::default().fg(read.theme.text_dim)),
85+
Span::styled("Enter", Style::default().fg(read.theme.accent)),
86+
Span::styled(" 确认", Style::default().fg(read.theme.text_dim)),
87+
Span::styled(" ", Style::default().fg(read.theme.text_dim)),
88+
Span::styled("Ctrl+P", Style::default().fg(read.theme.accent)),
89+
Span::styled(" 关闭", Style::default().fg(read.theme.text_dim)),
9190
])),
9291
status: None,
9392
}

src/screen/editor/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
mod context;
21
mod delete;
32
mod edit;
43
mod menu;
54
mod mode;
65
mod navigation;
76
mod session;
7+
mod state;
88
mod viewport;
9+
mod workbook_controller;
910

1011
use crossterm::event::{KeyEvent, MouseEvent};
1112
use ratatui::{Frame, layout::Rect, style::Style, widgets::Block};

src/screen/editor/mode.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
use crossterm::event::KeyEvent;
22
use ratatui::{Frame, layout::Rect, text::Line};
33

4-
use super::context::TableContext;
5-
use crate::model::cell::CellAddress;
6-
use crate::screen::EventResult;
4+
use super::{viewport::Viewport, workbook_controller::SelectionStats};
5+
use crate::{model::cell::CellAddress, screen::EventResult, theme::Theme};
76

87
/// 编辑器方向键方向。
98
#[derive(Clone, Copy)]
@@ -18,19 +17,31 @@ pub enum Direction {
1817
///
1918
/// `EditorView` 只暴露只读信息,避免模式直接修改编辑器状态。
2019
pub struct EditorView<'a> {
21-
ctx: &'a TableContext,
20+
selection: Option<Selection>,
21+
_marker: std::marker::PhantomData<&'a ()>,
2222
}
2323

2424
impl<'a> EditorView<'a> {
25-
pub fn new(ctx: &'a TableContext) -> Self {
26-
Self { ctx }
25+
pub fn new(selection: Option<Selection>) -> Self {
26+
Self {
27+
selection,
28+
_marker: std::marker::PhantomData,
29+
}
2730
}
2831

2932
pub fn selection(&self) -> Option<Selection> {
30-
self.ctx.selection().copied()
33+
self.selection
3134
}
3235
}
3336

37+
/// 模式渲染和 footer 需要的只读模型。
38+
pub struct EditorReadModel<'a> {
39+
pub theme: Theme,
40+
pub viewport: &'a Viewport,
41+
pub blink_visible: bool,
42+
pub selection_stats: Option<SelectionStats>,
43+
}
44+
3445
/// 编辑器模式产生的意图。
3546
///
3647
/// 模式只描述“用户想做什么”,具体如何修改工作簿、保存或切换屏幕,
@@ -151,11 +162,11 @@ pub trait Mode {
151162
/// 渲染模式专属内容,并返回留给表格区域的区域。
152163
///
153164
/// 例如菜单模式会占用右侧面板,编辑模式会占用底部输入行。
154-
fn render(&self, frame: &mut Frame, area: Rect, ctx: &TableContext) -> Rect;
165+
fn render(&self, frame: &mut Frame, area: Rect, read: EditorReadModel<'_>) -> Rect;
155166

156167
/// 返回当前模式的页脚文本。
157-
fn footer(&self, ctx: &TableContext) -> FooterLine {
158-
let _ = ctx;
168+
fn footer(&self, read: EditorReadModel<'_>) -> FooterLine {
169+
let _ = read;
159170
FooterLine::none()
160171
}
161172

src/screen/editor/navigation.rs

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ use ratatui::{Frame, layout::Rect, style::Style, text::Line};
33

44
use crate::screen::EventResult;
55

6-
use super::{
7-
context::TableContext,
8-
mode::{
9-
Direction, EditorIntent, EditorView, FooterLine, Mode, ModeKind, ModeResult, Selection,
10-
},
6+
use super::mode::{
7+
Direction, EditorIntent, EditorReadModel, EditorView, FooterLine, Mode, ModeKind, ModeResult,
8+
Selection,
119
};
1210

1311
/// 默认模式 —— 光标导航、单元格删除、进入编辑。
@@ -103,55 +101,55 @@ impl Mode for NavigationMode {
103101
}
104102
}
105103

106-
fn render(&self, _frame: &mut Frame, area: Rect, _ctx: &TableContext) -> Rect {
104+
fn render(&self, _frame: &mut Frame, area: Rect, _read: EditorReadModel<'_>) -> Rect {
107105
area
108106
}
109107

110-
fn footer(&self, ctx: &TableContext) -> FooterLine {
108+
fn footer(&self, read: EditorReadModel<'_>) -> FooterLine {
111109
use ratatui::text::Span;
112-
let hint = if let Some(stats) = ctx.selection_stats() {
110+
let hint = if let Some(stats) = read.selection_stats {
113111
Line::from(vec![
114-
Span::styled("平均值=", Style::default().fg(ctx.theme.accent)),
112+
Span::styled("平均值=", Style::default().fg(read.theme.accent)),
115113
Span::styled(
116114
format!(" {}", format_number(stats.average)),
117-
Style::default().fg(ctx.theme.text_dim),
115+
Style::default().fg(read.theme.text_dim),
118116
),
119-
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
120-
Span::styled("求和=", Style::default().fg(ctx.theme.accent)),
117+
Span::styled(" ", Style::default().fg(read.theme.text_dim)),
118+
Span::styled("求和=", Style::default().fg(read.theme.accent)),
121119
Span::styled(
122120
format!(" {}", format_number(stats.sum)),
123-
Style::default().fg(ctx.theme.text_dim),
121+
Style::default().fg(read.theme.text_dim),
124122
),
125-
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
126-
Span::styled("计数=", Style::default().fg(ctx.theme.accent)),
123+
Span::styled(" ", Style::default().fg(read.theme.text_dim)),
124+
Span::styled("计数=", Style::default().fg(read.theme.accent)),
127125
Span::styled(
128126
format!(" {}", stats.count),
129-
Style::default().fg(ctx.theme.text_dim),
127+
Style::default().fg(read.theme.text_dim),
130128
),
131129
])
132130
} else {
133131
Line::from(vec![
134-
Span::styled("Ctrl+S", Style::default().fg(ctx.theme.accent)),
135-
Span::styled(" 保存", Style::default().fg(ctx.theme.text_dim)),
136-
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
137-
Span::styled("Ctrl+P", Style::default().fg(ctx.theme.accent)),
138-
Span::styled(" 菜单", Style::default().fg(ctx.theme.text_dim)),
139-
Span::styled(" ", Style::default().fg(ctx.theme.text_dim)),
140-
Span::styled("Enter", Style::default().fg(ctx.theme.accent)),
141-
Span::styled(" 编辑", Style::default().fg(ctx.theme.text_dim)),
132+
Span::styled("Ctrl+S", Style::default().fg(read.theme.accent)),
133+
Span::styled(" 保存", Style::default().fg(read.theme.text_dim)),
134+
Span::styled(" ", Style::default().fg(read.theme.text_dim)),
135+
Span::styled("Ctrl+P", Style::default().fg(read.theme.accent)),
136+
Span::styled(" 菜单", Style::default().fg(read.theme.text_dim)),
137+
Span::styled(" ", Style::default().fg(read.theme.text_dim)),
138+
Span::styled("Enter", Style::default().fg(read.theme.accent)),
139+
Span::styled(" 编辑", Style::default().fg(read.theme.text_dim)),
142140
])
143141
};
144142

145143
FooterLine {
146144
hint: Some(hint),
147145
status: Some(Line::from(vec![
148-
Span::styled("[", Style::default().fg(ctx.theme.text_dim)),
146+
Span::styled("[", Style::default().fg(read.theme.text_dim)),
149147
Span::styled(
150-
ctx.cursor().display(),
151-
Style::default().fg(ctx.theme.accent),
148+
read.viewport.cursor().display(),
149+
Style::default().fg(read.theme.accent),
152150
),
153-
Span::styled(", 光标模式", Style::default().fg(ctx.theme.text_dim)),
154-
Span::styled("]", Style::default().fg(ctx.theme.text_dim)),
151+
Span::styled(", 光标模式", Style::default().fg(read.theme.text_dim)),
152+
Span::styled("]", Style::default().fg(read.theme.text_dim)),
155153
])),
156154
}
157155
}

0 commit comments

Comments
 (0)