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}