spreadsheet/
spreadsheet.rs

1// For a simpler boilerplate-less table, check the fltk-table crate
2
3use fltk::{
4    app, draw, enums, input,
5    prelude::{GroupExt, InputExt, TableExt, WidgetBase, WidgetExt, WidgetProps},
6    table, window,
7};
8use std::cell::RefCell;
9use std::rc::Rc;
10
11pub type MyData = Vec<Vec<String>>;
12
13// Needed to store cell information during the draw_cell call
14#[derive(Default)]
15struct CellData {
16    row: i32, // row
17    col: i32, // column
18    x: i32,
19    y: i32,
20    w: i32,
21    h: i32,
22}
23
24impl CellData {
25    pub fn select(&mut self, row: i32, col: i32, x: i32, y: i32, w: i32, h: i32) {
26        self.row = row;
27        self.col = col;
28        self.x = x;
29        self.y = y;
30        self.w = w;
31        self.h = h;
32    }
33}
34
35pub struct MyTable {
36    table: table::Table,
37    data: Rc<RefCell<MyData>>,
38    cell: Rc<RefCell<CellData>>,
39}
40
41impl MyTable {
42    pub fn new(mut inp: input::Input) -> Self {
43        let mut table = table::Table::default()
44            .with_size(800 - 10, 600 - 10)
45            .center_of_parent();
46        let data = Rc::from(RefCell::from(vec![vec![String::from(""); 26]; 28]));
47        let cell = Rc::from(RefCell::from(CellData::default()));
48
49        table.set_rows(28);
50        table.set_row_header(true);
51        table.set_row_resize(true);
52        table.set_cols(26);
53        table.set_col_header(true);
54        table.set_col_width_all(80);
55        table.set_col_resize(true);
56        table.end();
57
58        let cell_c = cell.clone();
59        let data_c = data.clone();
60
61        // Called when the table is drawn then when it's redrawn due to events
62        table.draw_cell(move |t, ctx, row, col, x, y, w, h| match ctx {
63            table::TableContext::StartPage => draw::set_font(enums::Font::Helvetica, 14),
64            table::TableContext::ColHeader => {
65                Self::draw_header(&format!("{}", (col + 65) as u8 as char), x, y, w, h)
66            } // Column titles
67            table::TableContext::RowHeader => {
68                Self::draw_header(&format!("{}", row + 1), x, y, w, h)
69            } // Row titles
70            table::TableContext::Cell => {
71                if t.is_selected(row, col) {
72                    cell_c.borrow_mut().select(row, col, x, y, w, h); // Captures the cell information
73                }
74                Self::draw_data(
75                    &data_c.borrow()[row as usize][col as usize].to_string(),
76                    x,
77                    y,
78                    w,
79                    h,
80                    t.is_selected(row, col),
81                );
82            }
83            _ => (),
84        });
85
86        let cell_c = cell.clone();
87        let data_c = data.clone();
88
89        table.handle(move |_, ev| match ev {
90            // Event::Push will happen before the focus is moved,
91            // thus giving the previous coordinates.
92            // Event::Released gives an accurate position
93            enums::Event::Released => {
94                let c = cell_c.borrow();
95                inp.resize(c.x, c.y, c.w, c.h);
96                inp.set_value(&data_c.borrow_mut()[c.row as usize][c.col as usize]);
97                inp.show();
98                inp.take_focus().ok();
99                inp.redraw();
100                true
101            }
102            _ => false,
103        });
104
105        Self { table, data, cell }
106    }
107
108    pub fn redraw(&mut self) {
109        self.table.redraw()
110    }
111
112    fn draw_header(txt: &str, x: i32, y: i32, w: i32, h: i32) {
113        draw::push_clip(x, y, w, h);
114        draw::draw_box(
115            enums::FrameType::ThinUpBox,
116            x,
117            y,
118            w,
119            h,
120            enums::Color::FrameDefault,
121        );
122        draw::set_draw_color(enums::Color::Black);
123        draw::set_font(enums::Font::Helvetica, 14);
124        draw::draw_text_boxed(txt, x, y, w, h, enums::Align::Center);
125        draw::pop_clip();
126    }
127
128    // The selected flag sets the color of the cell to a grayish color, otherwise white
129    fn draw_data(txt: &str, x: i32, y: i32, w: i32, h: i32, selected: bool) {
130        draw::push_clip(x, y, w, h);
131        if selected {
132            draw::set_draw_color(enums::Color::from_u32(0x00D3_D3D3));
133        } else {
134            draw::set_draw_color(enums::Color::White);
135        }
136        draw::draw_rectf(x, y, w, h);
137        draw::set_draw_color(enums::Color::Gray0);
138        draw::set_font(enums::Font::Helvetica, 14);
139        draw::draw_text_boxed(txt, x, y, w, h, enums::Align::Center);
140        draw::draw_rect(x, y, w, h);
141        draw::pop_clip();
142    }
143}
144
145fn main() {
146    let app = app::App::default().with_scheme(app::Scheme::Gtk);
147    let mut wind = window::Window::default().with_size(800, 600);
148    // We need an input widget
149    let mut inp = input::Input::default();
150    inp.hide();
151
152    let mut table = MyTable::new(inp.clone());
153
154    wind.make_resizable(true);
155    wind.end();
156    wind.show();
157
158    wind.handle(move |_, ev| match ev {
159        enums::Event::KeyDown => {
160            if app::event_key() == enums::Key::Enter {
161                // Press enter to store the data into the cell
162                let c = table.cell.borrow();
163                table.data.borrow_mut()[c.row as usize][c.col as usize] = inp.value();
164                inp.set_value("");
165                inp.hide();
166                return true;
167            }
168            table.redraw();
169            false
170        }
171        _ => false,
172    });
173
174    wind.set_callback(|_| {
175        if app::event() == enums::Event::Close {
176            // Close only when the close button is clicked
177            app::quit();
178        }
179    });
180
181    app.run().unwrap();
182}