azul_widgets/
table_view.rs

1//! Table view
2
3use std::{ops::Range, collections::BTreeMap};
4use azul_core::{
5    dom::{Dom, On, NodeData, DomString, NodeType},
6    callbacks::{
7        Ref, Callback, CallbackInfo, CallbackReturn,
8        IFrameCallbackInfo, IFrameCallbackReturn, DontRedraw,
9    },
10};
11
12#[derive(Debug, Clone)]
13pub struct TableView {
14    pub state: Ref<TableViewState>,
15    pub on_mouse_up: Callback,
16}
17
18impl Default for TableView {
19    fn default() -> Self {
20        Self {
21            state: Ref::default(),
22            on_mouse_up: Callback(Self::default_on_mouse_up),
23        }
24    }
25}
26
27#[derive(Debug, Clone)]
28pub struct TableViewState {
29    pub work_sheet: BTreeMap<usize, BTreeMap<usize, String>>,
30    pub column_width: f32,
31    pub row_height: f32,
32    pub selected_cell: Option<(usize, usize)>,
33}
34
35impl Default for TableViewState {
36    fn default() -> Self {
37        Self {
38            work_sheet: BTreeMap::default(),
39            column_width: 100.0,
40            row_height: 20.0,
41            selected_cell: None,
42        }
43    }
44}
45
46impl TableViewState {
47
48    /// Renders a cutout of the table from, horizontally from (col_start..col_end)
49    /// and vertically from (row_start..row_end)
50    pub fn render(&self, rows: Range<usize>, columns: Range<usize>) -> Dom {
51
52        // div.__azul-native-table-container
53        //     |-> div.__azul-native-table-column (Column 0)
54        //         |-> div.__azul-native-table-top-left-rect .__azul-native-table-column-name
55        //         '-> div.__azul-native-table-row-numbers .__azul-native-table-row
56        //
57        //     |-> div.__azul-native-table-column-container
58        //         |-> div.__azul-native-table-column (Column 1 ...)
59        //             |-> div.__azul-native-table-column-name
60        //             '-> div.__azul-native-table-row
61        //                 '-> div.__azul-native-table-cell
62
63        Dom::div()
64        .with_class("__azul-native-table-container")
65        .with_child(
66            Dom::div()
67            .with_class("__azul-native-table-row-number-wrapper")
68            .with_child(
69                // Empty rectangle at the top left of the table
70                Dom::div()
71                .with_class("__azul-native-table-top-left-rect")
72            )
73            .with_child(
74                // Row numbers (vertical) - "1", "2", "3"
75                (rows.start..rows.end.saturating_sub(1))
76                .map(|row_idx|
77                    NodeData::label(format!("{}", row_idx + 1))
78                    .with_classes(vec![DomString::Static("__azul-native-table-row")])
79                )
80                .collect::<Dom>()
81                .with_class("__azul-native-table-row-numbers")
82            )
83        )
84        .with_child(
85            columns
86            .map(|col_idx|
87                // Column name
88                Dom::new(NodeType::Div)
89                .with_class("__azul-native-table-column")
90                .with_child(Dom::label(column_name_from_number(col_idx)).with_class("__azul-native-table-column-name"))
91                .with_child(
92                    // row contents - if no content is given, they are simply empty
93                    (rows.start..rows.end)
94                    .map(|row_idx|
95                        NodeData::new(
96                            if let Some(data) = self.work_sheet.get(&col_idx).and_then(|col| col.get(&row_idx)) {
97                                NodeType::Label(DomString::Heap(data.clone()))
98                            } else {
99                                NodeType::Div
100                            }
101                        ).with_classes(vec![DomString::Static("__azul-native-table-cell")])
102                    )
103                    .collect::<Dom>()
104                    .with_class("__azul-native-table-rows")
105                )
106            )
107            .collect::<Dom>()
108            .with_class("__azul-native-table-column-container")
109            // current active selection (s)
110            .with_child(
111                Dom::div()
112                    .with_class("__azul-native-table-selection")
113                    .with_child(Dom::div().with_class("__azul-native-table-selection-handle"))
114            )
115        )
116    }
117
118    pub fn set_cell<I: Into<String>>(&mut self, x: usize, y: usize, value: I) {
119        self.work_sheet
120            .entry(x)
121            .or_insert_with(|| BTreeMap::new())
122            .insert(y, value.into());
123    }
124}
125
126impl TableView {
127
128    #[inline]
129    pub fn new(state: Ref<TableViewState>) -> Self {
130        Self { state, .. Default::default() }
131    }
132
133    #[inline]
134    pub fn with_state(self, state: Ref<TableViewState>) -> Self {
135        Self { state, .. self }
136    }
137
138    #[inline]
139    pub fn on_mouse_up(self, cb: Callback) -> Self {
140        Self { on_mouse_up: cb, .. self }
141    }
142
143    #[inline]
144    pub fn dom(self) -> Dom {
145        let upcasted_table_view = self.state.upcast();
146        Dom::iframe(Self::render_table_iframe_contents, upcasted_table_view.clone())
147            .with_class("__azul-native-table-iframe")
148            .with_callback(On::MouseUp, self.on_mouse_up.0, upcasted_table_view)
149    }
150
151    pub fn default_on_mouse_up(_info: CallbackInfo) -> CallbackReturn {
152        println!("table was clicked");
153        DontRedraw
154    }
155
156    fn render_table_iframe_contents(info: IFrameCallbackInfo) -> IFrameCallbackReturn {
157        let table_view_state = info.state.downcast::<TableViewState>()?;
158        let table_view_state = table_view_state.borrow();
159        let logical_size = info.bounds.get_logical_size();
160        let necessary_rows = (logical_size.height as f32 / table_view_state.row_height).ceil() as usize;
161        let necessary_columns = (logical_size.width as f32 / table_view_state.column_width).ceil() as usize;
162        Some(table_view_state.render(0..necessary_rows, 0..necessary_columns))
163    }
164}
165
166impl Into<Dom> for TableView {
167    fn into(self) -> Dom {
168        self.dom()
169    }
170}
171
172/// Maps an index number to a value, necessary for creating the column name:
173///
174/// ```no_run,ignore
175/// 0   -> A
176/// 25  -> Z
177/// 26  -> AA
178/// 27  -> AB
179/// ```
180///
181/// ... and so on. This implementation is very fast, takes ~50 to 100
182/// nanoseconds for 1 iteration due to almost pure-stack allocated data.
183/// For an explanation of the algorithm with comments, see:
184/// https://github.com/fschutt/street_index/blob/78b935a1303070947c0854b6d01f540ec298c9d5/src/gridconfig.rs#L155-L209
185pub fn column_name_from_number(num: usize) -> String {
186    const ALPHABET_LEN: usize = 26;
187    // usize::MAX is "GKGWBYLWRXTLPP" with a length of 15 characters
188    const MAX_LEN: usize = 15;
189
190    #[inline(always)]
191    fn u8_to_char(input: u8) -> u8 {
192        'A' as u8 + input
193    }
194
195    let mut result = [0;MAX_LEN + 1];
196    let mut multiple_of_alphabet = num / ALPHABET_LEN;
197    let mut character_count = 0;
198
199    while multiple_of_alphabet != 0 && character_count < MAX_LEN {
200        let remainder = (multiple_of_alphabet - 1) % ALPHABET_LEN;
201        result[(MAX_LEN - 1) - character_count] = u8_to_char(remainder as u8);
202        character_count += 1;
203        multiple_of_alphabet = (multiple_of_alphabet - 1) / ALPHABET_LEN;
204    }
205
206    result[MAX_LEN] = u8_to_char((num % ALPHABET_LEN) as u8);
207    let zeroed_characters = MAX_LEN.saturating_sub(character_count);
208    let slice = &result[zeroed_characters..];
209    unsafe { ::std::str::from_utf8_unchecked(slice) }.to_string()
210}
211
212#[test]
213fn test_column_name_from_number() {
214    assert_eq!(column_name_from_number(0), String::from("A"));
215    assert_eq!(column_name_from_number(1), String::from("B"));
216    assert_eq!(column_name_from_number(6), String::from("G"));
217    assert_eq!(column_name_from_number(26), String::from("AA"));
218    assert_eq!(column_name_from_number(27), String::from("AB"));
219    assert_eq!(column_name_from_number(225), String::from("HR"));
220}