use super::{Notebook, NotebookCell, CellId, CellType, ExecutionEngine, NotebookError, NotebookResult, AutoCompleteEngine, CompletionSuggestion};
use fltk::{prelude::*, *};
use std::collections::HashMap;
pub struct CellEditor {
container: group::Group,
type_label: frame::Frame,
editor: text::TextEditor,
output_display: text::TextDisplay,
cell_id: CellId,
cell_type: CellType,
is_selected: bool,
syntax_buffer: Option<text::TextBuffer>,
autocomplete_engine: AutoCompleteEngine,
}
impl CellEditor {
pub fn new(x: i32, y: i32, width: i32, height: i32, cell: &NotebookCell) -> Self {
let mut container = group::Group::new(x, y, width, height, None);
container.set_frame(enums::FrameType::BorderBox);
let mut type_label = frame::Frame::new(x + 5, y + 5, 80, 25, None);
type_label.set_label(&format!("[{}]", cell.cell_type.display_name()));
type_label.set_label_color(enums::Color::Blue);
type_label.set_label_font(enums::Font::CourierBold);
let mut editor = text::TextEditor::new(x + 5, y + 35, width - 10, height / 2 - 40, None);
let mut input_buffer = text::TextBuffer::default();
input_buffer.set_text(&cell.get_text());
editor.set_buffer(input_buffer);
editor.set_text_font(enums::Font::Courier);
editor.set_text_size(12);
let mut output_display = text::TextDisplay::new(x + 5, y + height / 2, width - 10, height / 2 - 5, None);
let mut output_buffer = text::TextBuffer::default();
if let Some(output) = cell.get_output() {
output_buffer.set_text(&output.get_text());
}
output_display.set_buffer(output_buffer);
output_display.set_text_font(enums::Font::Courier);
output_display.set_text_size(11);
output_display.set_color(enums::Color::from_rgb(248, 248, 248));
container.end();
let mut cell_editor = Self {
container,
type_label,
editor,
output_display,
cell_id: cell.id,
cell_type: cell.cell_type.clone(),
is_selected: false,
syntax_buffer: None,
autocomplete_engine: AutoCompleteEngine::new(),
};
cell_editor.setup_autocomplete();
cell_editor
}
pub fn cell_id(&self) -> CellId {
self.cell_id
}
pub fn cell_type(&self) -> &CellType {
&self.cell_type
}
pub fn get_input_text(&self) -> String {
self.editor.buffer().unwrap().text()
}
pub fn set_input_text(&mut self, text: &str) {
if let Some(mut buffer) = self.editor.buffer() {
buffer.set_text(text);
}
}
pub fn set_output_text(&mut self, text: &str) {
if let Some(mut buffer) = self.output_display.buffer() {
buffer.set_text(text);
}
}
pub fn set_selected(&mut self, selected: bool) {
self.is_selected = selected;
if selected {
self.container.set_color(enums::Color::from_rgb(230, 240, 255));
} else {
self.container.set_color(enums::Color::White);
}
self.container.redraw();
}
pub fn is_selected(&self) -> bool {
self.is_selected
}
pub fn set_cell_type(&mut self, cell_type: CellType) {
self.cell_type = cell_type.clone();
self.type_label.set_label(&format!("[{}]", cell_type.display_name()));
self.setup_syntax_highlighting();
self.type_label.redraw();
}
fn setup_syntax_highlighting(&mut self) {
match self.cell_type {
CellType::Code => {
self.editor.set_highlight_data(
text::TextBuffer::default(),
vec![
text::StyleTableEntry {
color: enums::Color::Blue,
font: enums::Font::Courier,
size: 12,
},
text::StyleTableEntry {
color: enums::Color::Red,
font: enums::Font::Courier,
size: 12,
},
text::StyleTableEntry {
color: enums::Color::DarkGreen,
font: enums::Font::Courier,
size: 12,
},
]
);
}
CellType::Markdown => {
self.editor.set_text_color(enums::Color::DarkBlue);
}
_ => {
self.editor.set_text_color(enums::Color::Black);
}
}
}
pub fn container(&self) -> &group::Group {
&self.container
}
pub fn container_mut(&mut self) -> &mut group::Group {
&mut self.container
}
pub fn focus(&mut self) {
let _ = self.editor.take_focus();
}
pub fn editor(&self) -> &text::TextEditor {
&self.editor
}
pub fn editor_mut(&mut self) -> &mut text::TextEditor {
&mut self.editor
}
fn setup_autocomplete(&mut self) {
}
pub fn show_completions(&mut self) {
if let Some(buffer) = self.editor.buffer() {
let text = buffer.text();
let cursor_pos = self.editor.insert_position();
let suggestions = self.autocomplete_engine.get_completions(&text, cursor_pos as usize);
if !suggestions.is_empty() {
self.show_completion_popup(suggestions);
}
}
}
fn show_completion_popup(&self, suggestions: Vec<CompletionSuggestion>) {
let mut popup = menu::MenuButton::new(
self.editor.x() + 10,
self.editor.y() + 30,
200,
20,
None
);
for (i, suggestion) in suggestions.iter().enumerate() {
popup.add_emit(
&suggestion.label,
enums::Shortcut::None,
menu::MenuFlag::Normal,
app::Sender::<String>::get(),
suggestion.text.clone(),
);
if i >= 10 {
break; }
}
popup.popup();
}
pub fn autocomplete_engine(&self) -> &AutoCompleteEngine {
&self.autocomplete_engine
}
pub fn autocomplete_engine_mut(&mut self) -> &mut AutoCompleteEngine {
&mut self.autocomplete_engine
}
}
pub struct NotebookGUI {
window: window::Window,
menu_bar: menu::MenuBar,
toolbar: group::Group,
scroll: group::Scroll,
cell_container: group::Pack,
status_bar: frame::Frame,
notebook: Option<Notebook>,
execution_engine: ExecutionEngine,
cell_editors: Vec<CellEditor>,
current_cell: Option<usize>,
shortcuts: HashMap<i32, Box<dyn Fn(&mut Self)>>,
}
impl NotebookGUI {
pub fn new() -> Self {
let _app = app::App::default();
app::set_scheme(app::Scheme::Gtk);
let mut window = window::Window::new(100, 100, 1000, 700, "Yufmath Notebook");
window.set_color(enums::Color::White);
let menu_bar = menu::MenuBar::new(0, 0, 1000, 30, None);
let mut toolbar = group::Group::new(0, 30, 1000, 40, None);
toolbar.set_color(enums::Color::from_rgb(240, 240, 240));
let _new_code_btn = button::Button::new(10, 35, 80, 30, "新建代码");
let _new_md_btn = button::Button::new(100, 35, 80, 30, "新建文档");
let _run_btn = button::Button::new(190, 35, 60, 30, "运行");
let _run_all_btn = button::Button::new(260, 35, 80, 30, "运行全部");
let _save_btn = button::Button::new(350, 35, 60, 30, "保存");
toolbar.end();
let mut scroll = group::Scroll::new(0, 70, 1000, 600, None);
scroll.set_type(group::ScrollType::Vertical);
let mut cell_container = group::Pack::new(10, 80, 980, 580, None);
cell_container.set_type(group::PackType::Vertical);
cell_container.set_spacing(10);
cell_container.end();
scroll.end();
let mut status_bar = frame::Frame::new(0, 670, 1000, 30, "就绪");
status_bar.set_color(enums::Color::from_rgb(240, 240, 240));
status_bar.set_align(enums::Align::Left | enums::Align::Inside);
window.end();
window.show();
let mut gui = Self {
window,
menu_bar,
toolbar,
scroll,
cell_container,
status_bar,
notebook: None,
execution_engine: ExecutionEngine::new(),
cell_editors: Vec::new(),
current_cell: None,
shortcuts: HashMap::new(),
};
gui.setup_menu();
gui.setup_callbacks();
gui.setup_shortcuts();
gui
}
fn setup_menu(&mut self) {
self.menu_bar.add_emit(
"文件/新建\t",
enums::Shortcut::Ctrl | 'n',
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
1001,
);
self.menu_bar.add_emit(
"文件/打开...\t",
enums::Shortcut::Ctrl | 'o',
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
1002,
);
self.menu_bar.add_emit(
"文件/保存\t",
enums::Shortcut::Ctrl | 's',
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
1003,
);
self.menu_bar.add_emit(
"文件/另存为...\t",
enums::Shortcut::Ctrl | enums::Shortcut::Shift | 's',
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
1004,
);
self.menu_bar.add_emit(
"文件/退出\t",
enums::Shortcut::Ctrl | 'q',
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
1099,
);
self.menu_bar.add_emit(
"编辑/新建代码单元格\t",
enums::Shortcut::Ctrl | 'n',
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
2001,
);
self.menu_bar.add_emit(
"编辑/新建文档单元格\t",
enums::Shortcut::Ctrl | 'm',
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
2002,
);
self.menu_bar.add_emit(
"编辑/删除单元格\t",
enums::Shortcut::Ctrl | 'd',
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
2003,
);
self.menu_bar.add_emit(
"运行/执行当前单元格\t",
enums::Shortcut::Ctrl | enums::Shortcut::from_key(enums::Key::Enter),
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
3001,
);
self.menu_bar.add_emit(
"运行/执行并新建单元格\t",
enums::Shortcut::Shift | enums::Shortcut::from_key(enums::Key::Enter),
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
3002,
);
self.menu_bar.add_emit(
"运行/执行所有单元格\t",
enums::Shortcut::Ctrl | 'r',
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
3003,
);
self.menu_bar.add_emit(
"帮助/关于\t",
enums::Shortcut::None,
menu::MenuFlag::Normal,
app::Sender::<i32>::get(),
9001,
);
}
fn setup_callbacks(&mut self) {
}
fn setup_shortcuts(&mut self) {
}
pub fn set_notebook(&mut self, notebook: Notebook) -> NotebookResult<()> {
self.notebook = Some(notebook);
self.refresh_cells()?;
self.update_title();
Ok(())
}
pub fn get_notebook(&self) -> Option<&Notebook> {
self.notebook.as_ref()
}
fn refresh_cells(&mut self) -> NotebookResult<()> {
self.cell_container.clear();
self.cell_editors.clear();
if let Some(notebook) = &self.notebook {
let mut y_offset = 10;
for (index, cell) in notebook.get_cells().iter().enumerate() {
let mut cell_editor = CellEditor::new(
10,
y_offset,
960,
150,
cell
);
if Some(index) == self.current_cell {
cell_editor.set_selected(true);
}
self.cell_container.add(&cell_editor.container);
self.cell_editors.push(cell_editor);
y_offset += 160;
}
}
self.cell_container.redraw();
self.scroll.redraw();
Ok(())
}
fn update_title(&mut self) {
let title = if let Some(notebook) = &self.notebook {
format!("Yufmath Notebook - {}", notebook.metadata.title)
} else {
"Yufmath Notebook".to_string()
};
self.window.set_label(&title);
}
pub fn create_code_cell(&mut self) -> NotebookResult<()> {
self.create_cell(CellType::Code)
}
pub fn create_markdown_cell(&mut self) -> NotebookResult<()> {
self.create_cell(CellType::Markdown)
}
fn create_cell(&mut self, cell_type: CellType) -> NotebookResult<()> {
if let Some(notebook) = &mut self.notebook {
let cell = match cell_type {
CellType::Code => NotebookCell::new_code("".to_string()),
CellType::Markdown => NotebookCell::new_markdown("".to_string()),
CellType::Text => NotebookCell::new_text("".to_string()),
CellType::Output => return Err(NotebookError::Cell("不能手动创建输出单元格".to_string())),
};
let insert_index = self.current_cell.map(|i| i + 1).unwrap_or(notebook.cell_count());
notebook.insert_cell(insert_index, cell)?;
self.refresh_cells()?;
self.current_cell = Some(insert_index);
if let Some(editor) = self.cell_editors.get_mut(insert_index) {
editor.focus();
}
self.set_status(&format!("创建了新的{}单元格", cell_type.display_name()));
}
Ok(())
}
pub fn delete_current_cell(&mut self) -> NotebookResult<()> {
if let Some(current_index) = self.current_cell {
if let Some(notebook) = &mut self.notebook {
notebook.remove_cell(current_index)?;
if current_index >= notebook.cell_count() && notebook.cell_count() > 0 {
self.current_cell = Some(notebook.cell_count() - 1);
} else if notebook.cell_count() == 0 {
self.current_cell = None;
}
self.refresh_cells()?;
self.set_status("删除了单元格");
}
}
Ok(())
}
pub fn execute_current_cell(&mut self) -> NotebookResult<()> {
if let Some(current_index) = self.current_cell {
self.execute_cell(current_index)
} else {
Ok(())
}
}
fn execute_cell(&mut self, index: usize) -> NotebookResult<()> {
if let Some(notebook) = &mut self.notebook {
if let Some(cell) = notebook.get_cell_mut(index) {
if let Some(editor) = self.cell_editors.get(index) {
let input_text = editor.get_input_text();
cell.set_text(input_text);
}
let result = self.execution_engine.execute_cell(cell)?;
if let Some(editor) = self.cell_editors.get_mut(index) {
match &result {
super::ExecutionResult::Success { value, execution_time, .. } => {
editor.set_output_text(&value);
self.set_status(&format!("执行成功 ({:.2}ms)", execution_time.as_millis()));
}
super::ExecutionResult::Error { error, .. } => {
editor.set_output_text(&format!("错误: {}", error));
self.set_status(&format!("执行错误: {}", error));
}
super::ExecutionResult::Skipped => {
self.set_status("跳过执行(非代码单元格)");
}
super::ExecutionResult::Cancelled => {
self.set_status("执行被取消");
}
}
}
}
}
Ok(())
}
pub fn execute_and_create_cell(&mut self) -> NotebookResult<()> {
self.execute_current_cell()?;
self.create_code_cell()?;
Ok(())
}
pub fn execute_all_cells(&mut self) -> NotebookResult<()> {
if let Some(notebook) = &self.notebook {
let cell_count = notebook.cell_count();
for i in 0..cell_count {
self.execute_cell(i)?;
self.set_status(&format!("执行进度: {}/{}", i + 1, cell_count));
app::App::default().wait();
}
self.set_status(&format!("执行完成: {} 个单元格", cell_count));
}
Ok(())
}
pub fn save_notebook(&mut self) -> NotebookResult<()> {
if let Some(notebook) = &mut self.notebook {
for (index, editor) in self.cell_editors.iter().enumerate() {
if let Some(cell) = notebook.get_cell_mut(index) {
let input_text = editor.get_input_text();
cell.set_text(input_text);
}
}
if let Some(path) = notebook.get_file_path().cloned() {
super::NotebookSerializer::save_to_file(notebook, path)?;
self.set_status("笔记本已保存");
} else {
self.show_save_dialog()?;
}
}
Ok(())
}
fn show_save_dialog(&mut self) -> NotebookResult<()> {
let mut dialog = dialog::FileDialog::new(dialog::FileDialogType::BrowseSaveFile);
dialog.set_filter("*.ynb");
dialog.show();
if let Some(path) = dialog.filename().to_str() {
if let Some(notebook) = &mut self.notebook {
notebook.set_file_path(std::path::PathBuf::from(path));
super::NotebookSerializer::save_to_file(notebook, path.to_string())?;
self.set_status("笔记本已保存");
self.update_title();
}
}
Ok(())
}
pub fn show_open_dialog(&mut self) -> NotebookResult<()> {
let mut dialog = dialog::FileDialog::new(dialog::FileDialogType::BrowseFile);
dialog.set_filter("*.ynb");
dialog.show();
if let Some(path) = dialog.filename().to_str() {
let notebook = super::NotebookDeserializer::load_from_file(path.to_string())?;
self.set_notebook(notebook)?;
self.set_status(&format!("已打开笔记本: {}", path));
}
Ok(())
}
pub fn convert_current_cell(&mut self, new_type: CellType) -> NotebookResult<()> {
if let Some(current_index) = self.current_cell {
if let Some(notebook) = &mut self.notebook {
if let Some(cell) = notebook.get_cell_mut(current_index) {
if let Some(editor) = self.cell_editors.get(current_index) {
let input_text = editor.get_input_text();
cell.set_text(input_text);
}
cell.cell_type = new_type.clone();
if let Some(editor) = self.cell_editors.get_mut(current_index) {
editor.set_cell_type(new_type.clone());
}
self.set_status(&format!("单元格已转换为{}", new_type.display_name()));
}
}
}
Ok(())
}
pub fn move_to_previous_cell(&mut self) {
if let Some(current) = self.current_cell {
if current > 0 {
self.set_current_cell(Some(current - 1));
}
}
}
pub fn move_to_next_cell(&mut self) {
if let Some(current) = self.current_cell {
if let Some(notebook) = &self.notebook {
if current < notebook.cell_count() - 1 {
self.set_current_cell(Some(current + 1));
}
}
}
}
fn set_current_cell(&mut self, index: Option<usize>) {
if let Some(old_index) = self.current_cell {
if let Some(editor) = self.cell_editors.get_mut(old_index) {
editor.set_selected(false);
}
}
self.current_cell = index;
if let Some(new_index) = index {
if let Some(editor) = self.cell_editors.get_mut(new_index) {
editor.set_selected(true);
editor.focus();
}
}
}
fn set_status(&mut self, message: &str) {
self.status_bar.set_label(message);
self.status_bar.redraw();
}
pub fn show_about(&self) {
dialog::message_default("关于 Yufmath Notebook\n\n基于 FLTK 的计算机代数系统笔记本界面\n版本 0.1.0");
}
pub fn run(&mut self) -> NotebookResult<()> {
while app::wait() {
if let Some(msg) = app::Receiver::<i32>::get().recv() {
match msg {
1001 => { let notebook = Notebook::with_title("新笔记本".to_string());
self.set_notebook(notebook).ok();
}
1002 => { self.show_open_dialog().ok();
}
1003 => { self.save_notebook().ok();
}
1004 => { self.show_save_dialog().ok();
}
1099 => { break;
}
2001 => { self.create_code_cell().ok();
}
2002 => { self.create_markdown_cell().ok();
}
2003 => { self.delete_current_cell().ok();
}
3001 => { self.execute_current_cell().ok();
}
3002 => { self.execute_and_create_cell().ok();
}
3003 => { self.execute_all_cells().ok();
}
9001 => { self.show_about();
}
_ => {}
}
}
}
Ok(())
}
}
impl Default for NotebookGUI {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_editor_creation() {
let cell = NotebookCell::new_code("test code".to_string());
let editor = CellEditor::new(0, 0, 400, 200, &cell);
assert_eq!(editor.cell_id(), cell.id);
assert_eq!(editor.cell_type(), &CellType::Code);
assert_eq!(editor.get_input_text(), "test code");
assert!(!editor.is_selected());
}
#[test]
fn test_cell_editor_selection() {
let cell = NotebookCell::new_code("test".to_string());
let mut editor = CellEditor::new(0, 0, 400, 200, &cell);
assert!(!editor.is_selected());
editor.set_selected(true);
assert!(editor.is_selected());
editor.set_selected(false);
assert!(!editor.is_selected());
}
#[test]
fn test_cell_editor_type_change() {
let cell = NotebookCell::new_code("test".to_string());
let mut editor = CellEditor::new(0, 0, 400, 200, &cell);
assert_eq!(editor.cell_type(), &CellType::Code);
editor.set_cell_type(CellType::Markdown);
assert_eq!(editor.cell_type(), &CellType::Markdown);
}
#[test]
fn test_notebook_gui_creation() {
if std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok() {
let gui = NotebookGUI::new();
assert!(gui.notebook.is_none());
assert!(gui.current_cell.is_none());
assert!(gui.cell_editors.is_empty());
}
}
}