polars_view/
data_format.rs

1use egui::{Align, DragValue, Grid, Layout, Ui, Vec2};
2use polars::prelude::*;
3
4use std::{collections::HashMap, fmt::Debug, sync::LazyLock};
5
6// --- Constants ---
7
8/// A static, lazily initialized map defining the *default* text alignments
9/// for various Polars `DataType`s used in the `egui` table.
10///
11/// 1. **Purpose**: Provides sensible default alignments (e.g., numbers right, text left).
12/// 2. **Usage**: Read by `get_decimal_and_layout` to determine cell `egui::Layout`.
13/// 3. **Override**: User preferences in `DataFormat.alignments` take precedence.
14/// 4. **Implementation**: `LazyLock` ensures thread-safe, lazy, one-time initialization.
15pub static DEFAULT_ALIGNMENTS: LazyLock<HashMap<DataType, Align>> = LazyLock::new(|| {
16    HashMap::from([
17        // Numerical types: Right-aligned.
18        (DataType::Float32, Align::RIGHT),
19        (DataType::Float64, Align::RIGHT),
20        // Integer/Temporal/Boolean types: Centered.
21        (DataType::Int8, Align::Center),
22        (DataType::Int16, Align::Center),
23        (DataType::Int32, Align::Center),
24        (DataType::Int64, Align::Center),
25        (DataType::UInt8, Align::Center),
26        (DataType::UInt16, Align::Center),
27        (DataType::UInt32, Align::Center),
28        (DataType::UInt64, Align::Center),
29        (DataType::Date, Align::Center),
30        (DataType::Time, Align::Center),
31        (
32            DataType::Datetime(TimeUnit::Milliseconds, None),
33            Align::Center,
34        ),
35        (
36            DataType::Datetime(TimeUnit::Nanoseconds, None),
37            Align::Center,
38        ),
39        (DataType::Duration(TimeUnit::Milliseconds), Align::Center),
40        (DataType::Duration(TimeUnit::Nanoseconds), Align::Center),
41        (DataType::Boolean, Align::Center),
42        // Textual/Binary data: Left-aligned.
43        (DataType::String, Align::LEFT),
44        (DataType::Binary, Align::LEFT),
45    ])
46});
47
48// --- Data Structures ---
49
50/// Holds user-configurable settings for data presentation in the table.
51///
52/// ## State Management & Interaction:
53/// - **UI State**: An instance is held in `PolarsViewApp` (`layout.rs`) as `applied_format`, representing the
54///   current UI configuration. `render_format` modifies this instance directly.
55/// - **Data State**: An `Arc<DataFormat>` is stored within each `DataFrameContainer` (`container.rs`),
56///   capturing the format settings active when that data state was created (e.g., after load or sort).
57/// - **Update Flow**: Changes in `render_format` are detected, triggering an async `DataFrameContainer::update_format`
58///   call in `layout.rs`. This creates a new `DataFrameContainer` with the updated `Arc<DataFormat>`,
59///   ensuring the table re-renders with the new settings.
60#[derive(Debug, Clone, PartialEq)]
61pub struct DataFormat {
62    /// Stores the *current* alignment setting for each `DataType`, overriding `DEFAULT_ALIGNMENTS`.
63    /// - Modified by UI widgets rendered by `render_alignment_panel`.
64    /// - Read by `get_decimal_and_layout` to determine `egui::Layout` for table cells.
65    pub alignments: HashMap<DataType, Align>,
66
67    /// Controls the table column sizing strategy (`container.rs::build_table`).
68    /// - `true`: `Column::auto()` (content-based, potentially slower).
69    /// - `false` (Default): `Column::initial()` (uniform fixed widths, faster).
70    /// - **Important**: Toggling this changes the `TableBuilder` ID salt in `container.rs::build_table`,
71    ///   forcing egui to discard cached column widths and apply the new strategy.
72    pub auto_col_width: bool,
73
74    /// Number of decimal places for displaying floats (`Float32`, `Float64`).
75    /// - Modified by the `DragValue` in `render_decimal_input`.
76    /// - Used by `get_decimal_and_layout` / `data_container.rs::format_cell_value`.
77    ///   (Note: `decimal_and_layout_v2` might override for specific columns).
78    pub decimal: usize,
79
80    /// User-configurable *additional* vertical padding for the table header row.
81    /// - Applied in `container.rs::build_table` when calculating header height.
82    /// - Modified by `DragValue` in `render_header_padding_input` (if `use_enhanced_header`).
83    pub header_padding: f32,
84
85    /// Toggles the table header's visual style and click behavior.
86    /// - Modified by checkbox in `render_header`.
87    /// - Read by `container.rs::render_table_header` to choose between:
88    ///   - `true` (Default): Enhanced (styled text, wrapping, icon-only sort click).
89    ///   - `false`: Simple (plain button, non-wrapping, full button sort click).
90    pub use_enhanced_header: bool,
91}
92
93// --- Implementations ---
94
95impl Default for DataFormat {
96    /// Creates a `DataFormat` with default settings.
97    /// Initializes `alignments` by cloning the `DEFAULT_ALIGNMENTS` map.
98    fn default() -> Self {
99        DataFormat {
100            alignments: DEFAULT_ALIGNMENTS.clone(), // Clone defaults for this instance.
101            auto_col_width: true,                   // Default automatic content-based sizing.
102            decimal: 2,                             // Default float precision.
103            header_padding: 5.0,                    // Default extra padding for enhanced header.
104            use_enhanced_header: true,              // Default to enhanced header style.
105        }
106    }
107}
108
109impl DataFormat {
110    /// Gets the default header padding value defined in `DataFormat::default()`.
111    /// Used internally, e.g., by container.rs when `use_enhanced_header` is false.
112    pub fn get_default_padding(&self) -> f32 {
113        // Retrieve the padding value from a temporary default instance.
114        Self::default().header_padding
115    }
116
117    /// Renders UI controls for modifying format settings in the side panel ("Format" section).
118    ///
119    /// ## Change Detection & Update Flow:
120    /// 1. **Capture Initial State:** Clones `self` before rendering widgets (`format_former`).
121    /// 2. **Render Widgets:** Widgets (`checkbox`, `radio_value`, `DragValue`) bind to `&mut self`
122    ///    and modify its fields directly based on user interaction within this frame.
123    /// 3. **Compare States:** After rendering all widgets, compares the potentially modified `self`
124    ///    with `format_former`.
125    /// 4. **Signal Change:** If `self != format_former`, returns `Some(self.clone())`. `layout.rs` uses
126    ///    this signal to trigger an asynchronous `DataFrameContainer::update_format` task, applying
127    ///    the changes efficiently without reloading data. If no change, returns `None`.
128    ///
129    /// ### Arguments
130    /// * `ui`: Mutable reference to the `egui::Ui` context for drawing.
131    ///
132    /// ### Returns
133    /// * `Option<DataFormat>`: `Some(updated_format)` if a setting was changed, otherwise `None`.
134    pub fn render_format(&mut self, ui: &mut Ui) -> Option<DataFormat> {
135        // 1. Capture the state *before* potential modifications.
136        let format_former = self.clone();
137        let mut result = None; // Assume no change initially.
138
139        // Layout setup for the format section UI.
140        let width_max = ui.available_width();
141        let width_min = 200.0; // Minimum reasonable width for the controls.
142        let grid = Grid::new("data_format_grid")
143            .num_columns(2) // Labels in col 0, widgets in col 1.
144            .spacing([10.0, 20.0])
145            .striped(true);
146
147        ui.allocate_ui_with_layout(
148            Vec2::new(width_max, ui.available_height()),
149            Layout::top_down(Align::LEFT),
150            |ui| {
151                // 2. Render UI Controls within the Grid. These potentially modify `self`.
152                grid.show(ui, |ui| {
153                    ui.set_min_width(width_min);
154
155                    self.render_alignment_panel(ui); // Modifies `self.alignments`.
156                    self.render_decimal_input(ui); // Modifies `self.decimal`.
157                    self.render_auto_col(ui); // Modifies `self.auto_col_width`.
158                    self.render_header(ui); // Modifies `self.use_enhanced_header`.
159
160                    // Only show padding control if the enhanced header is active.
161                    if self.use_enhanced_header {
162                        self.render_header_padding_input(ui); // Modifies `self.header_padding`.
163                    }
164
165                    // 3. Detect Changes after all widgets rendered for this frame.
166                    if *self != format_former {
167                        result = Some(self.clone()); // Signal the change with the new state.
168                        tracing::debug!(
169                            "Format change detected in render_format. New state: {:#?}",
170                            self
171                        );
172                    }
173                });
174            },
175        );
176
177        // 4. Return the result, signalling to layout.rs whether an update is needed.
178        result
179    }
180
181    /// Renders the collapsible UI section for configuring text alignment per `DataType`.
182    ///
183    /// Uses a nested `egui::Grid` within a `CollapsingHeader`. Calls `show_alignment_row`
184    /// for each data type, passing `&mut self.alignments` which is modified directly by the
185    /// radio buttons in the helper function.
186    fn render_alignment_panel(&mut self, ui: &mut Ui) {
187        ui.label("Alignment:"); // Section label.
188
189        // Group alignment settings.
190        ui.collapsing("Data Types", |ui| {
191            // Nested grid for layout (DataType | Left | Center | Right).
192            Grid::new("align_grid")
193                .num_columns(4)
194                .spacing([10.0, 10.0])
195                .striped(true)
196                .show(ui, |ui| {
197                    // Render rows for relevant DataTypes.
198                    self.show_alignment_row(ui, &DataType::Float64);
199                    self.show_alignment_row(ui, &DataType::Float32);
200                    self.show_alignment_row(ui, &DataType::Int64);
201                    self.show_alignment_row(ui, &DataType::Int32);
202                    self.show_alignment_row(ui, &DataType::Int16);
203                    self.show_alignment_row(ui, &DataType::Int8);
204                    self.show_alignment_row(ui, &DataType::UInt64);
205                    self.show_alignment_row(ui, &DataType::UInt32);
206                    self.show_alignment_row(ui, &DataType::UInt16);
207                    self.show_alignment_row(ui, &DataType::UInt8);
208                    self.show_alignment_row(ui, &DataType::Date);
209                    self.show_alignment_row(ui, &DataType::Time);
210                    self.show_alignment_row(ui, &DataType::Boolean);
211                    self.show_alignment_row(ui, &DataType::Binary);
212                    self.show_alignment_row(ui, &DataType::String);
213                });
214        });
215        ui.end_row(); // End row in the outer format grid.
216    }
217
218    /// Renders a single row in the alignment grid for a specific `DataType`.
219    fn show_alignment_row(&mut self, ui: &mut Ui, data_type: &DataType) {
220        // 1. Get/Insert alignment setting for this data type. Defaults to Left if not present.
221        let current_align: &mut Align = self
222            .alignments
223            .entry(data_type.clone())
224            .or_insert(Align::LEFT);
225
226        // 2. Display the DataType name.
227        ui.label(format!("{data_type:?}"));
228
229        // 3. Render radio buttons. `radio_value` updates `current_align` (mutates map) on click.
230        ui.radio_value(current_align, Align::LEFT, "Left")
231            .on_hover_text("Align column content to the left.");
232        ui.radio_value(current_align, Align::Center, "Center")
233            .on_hover_text("Align column content to the center.");
234        ui.radio_value(current_align, Align::RIGHT, "Right")
235            .on_hover_text("Align column content to the right.");
236
237        ui.end_row(); // End this row in the alignment grid.
238    }
239
240    /// Renders the `DragValue` widget for setting the number of decimal places (`self.decimal`).
241    /// Modifies `self.decimal` directly based on user input.
242    fn render_decimal_input(&mut self, ui: &mut Ui) {
243        let decimal_max = 10;
244        ui.label("Decimals:");
245        // Bind DragValue to `self.decimal`.
246        ui.add(
247            DragValue::new(&mut self.decimal)
248                .speed(1) // Integer steps.
249                .range(0..=decimal_max), // Sensible range for display.
250        )
251        .on_hover_text(format!(
252            "Number of decimal places for floating-point numbers.\n\
253            Maximum decimal places: {decimal_max}"
254        ));
255        ui.end_row();
256    }
257
258    /// Renders the checkbox for toggling automatic column width (`self.auto_col_width`).
259    /// Modifies `self.auto_col_width` directly.
260    ///
261    /// Toggling this is detected by `render_format`, triggering `update_format`. The change
262    /// in `auto_col_width` causes `container.rs::build_table` to use a different `egui::Id`,
263    /// resetting table layout state (like manual widths) and applying the new sizing mode.
264    fn render_auto_col(&mut self, ui: &mut Ui) {
265        ui.label("Auto Col Width:");
266        // Bind checkbox to `self.auto_col_width`.
267        ui.checkbox(&mut self.auto_col_width, "").on_hover_text(
268            "Enable: Size columns based on content (slower).\n\
269            Disable: Use uniform initial widths (faster), allows manual resize.",
270        );
271        ui.end_row();
272    }
273
274    /// Renders the checkbox for toggling the table header style (`self.use_enhanced_header`).
275    /// Modifies `self.use_enhanced_header` directly. Affects rendering in `container.rs::render_table_header`.
276    fn render_header(&mut self, ui: &mut Ui) {
277        ui.label("Enhanced Header:");
278        // Bind checkbox to `self.use_enhanced_header`.
279        ui.checkbox(&mut self.use_enhanced_header, "")
280            .on_hover_text(
281                "Enable: Styled, wrapping text with icon-only sort click.\n\
282                Disable: Simpler button header.",
283            );
284        ui.end_row();
285    }
286
287    /// Renders the `DragValue` widget for adjusting header padding (`self.header_padding`).
288    /// Shown conditionally based on `self.use_enhanced_header`.
289    /// Modifies `self.header_padding` directly. Affects header height calculation in `container.rs::build_table`.
290    fn render_header_padding_input(&mut self, ui: &mut Ui) {
291        let heigth_max = 800.0;
292        ui.label("Header Padding:");
293        // Bind DragValue to `self.header_padding`.
294        ui.add(
295            DragValue::new(&mut self.header_padding)
296                .speed(0.5)
297                .range(0.0..=heigth_max) // Reasonable padding range.
298                .suffix(" px"), // Display units.
299        )
300        .on_hover_text(format!(
301            "Additional vertical padding for the enhanced table header.\n\
302            Maximum header padding: {:.*} px",
303            1, heigth_max
304        ));
305        ui.end_row();
306    }
307}
308
309//----------------------------------------------------------------------------//
310//                                   Tests                                    //
311//----------------------------------------------------------------------------//
312
313/// Run tests with:
314/// `cargo test -- --show-output tests_format`
315#[cfg(test)]
316mod tests_format {
317    use polars::prelude::*;
318
319    #[test]
320    fn test_quoted_bool_ints() -> PolarsResult<()> {
321        let csv = r#"
322foo,bar,baz
3231,"4","false"
3243,"5","false"
3255,"6","true"
326"#;
327        let file = std::io::Cursor::new(csv); // Create a cursor for the in-memory CSV data.
328        let df = CsvReader::new(file).finish()?; // Read the CSV data into a DataFrame.
329        println!("df = {df}");
330
331        // Define the expected DataFrame.
332        let expected = df![
333            "foo" => [1, 3, 5],
334            "bar" => [4, 5, 6],
335            "baz" => [false, false, true],
336        ]?;
337
338        // Assert that the loaded DataFrame equals the expected DataFrame.
339        assert!(df.equals_missing(&expected));
340        Ok(())
341    }
342}