polars_view/layout.rs
1use crate::{
2 DataContainer, DataFilter, DataFormat, Error, FileInfo, MyStyle, Notification, PolarsViewError,
3 PolarsViewResult, Settings, SortBy, open_file, save, save_as,
4};
5
6use egui::{
7 CentralPanel, Color32, Context, FontId, Frame, Grid, Key, KeyboardShortcut, Layout, MenuBar,
8 Modifiers, RichText, ScrollArea, SidePanel, Stroke, TopBottomPanel, ViewportCommand,
9 style::Visuals,
10};
11use std::{future::Future, sync::Arc};
12use tokio::sync::oneshot::{self, Receiver, error::TryRecvError};
13use tracing::error;
14
15// --- Type Aliases ---
16
17/// Type alias for a `Result` specifically wrapping a `DataContainer` on success.
18/// Simplifies function signatures involving potential data loading/processing errors.
19pub type ContainerResult = PolarsViewResult<DataContainer>;
20
21/// Type alias for a boxed, dynamically dispatched Future that yields a `ContainerResult`.
22/// This allows storing and managing different asynchronous operations (load, sort, format)
23/// that all eventually produce a `DataContainer` or an error.
24/// - `dyn Future`: Dynamic dispatch for different future types.
25/// - `Output = ContainerResult`: The future resolves to our specific result type.
26/// - `+ Unpin`: Required for `async`/`await` usage in certain contexts.
27/// - `+ Send + 'static`: Necessary bounds for futures used across threads (like with `tokio::spawn`).
28pub type DataFuture = Box<dyn Future<Output = ContainerResult> + Unpin + Send + 'static>;
29
30// --- Constants ---
31
32// Define keyboard shortcuts for common actions using `egui`'s `KeyboardShortcut`.
33const CTRL_O: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::O); // Ctrl+O for Open File
34const CTRL_S: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::S); // Ctrl+S for Save File
35const CTRL_A: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::A); // Ctrl+A for Save As...
36
37// --- Main Application Struct ---
38
39/// The main application struct for PolarsView, holding the entire UI and async state.
40pub struct PolarsViewApp {
41 /// Holds the currently loaded data and its associated view state (`DataContainer`).
42 /// `None` if no data is loaded. `Arc` allows efficient sharing with async tasks.
43 pub data_container: Option<Arc<DataContainer>>,
44
45 /// Stores the state of the data loading/filtering parameters *last applied*.
46 /// Used by the side panel UI (`render_query`) to detect changes made by the user
47 /// to settings like SQL query, delimiter, etc. **Does not contain sort info.**
48 pub applied_filter: DataFilter,
49
50 /// Stores the state of the data formatting settings *last applied*.
51 /// Used by the side panel UI (`render_format`) to detect changes to display settings.
52 pub applied_format: DataFormat,
53
54 /// Info extracted from the currently loaded file.
55 pub file_info: Option<FileInfo>,
56
57 /// Optional Notification window for displaying errors or settings dialogs.
58 pub notification: Option<Box<dyn Notification>>,
59
60 /// Tokio runtime instance for managing asynchronous operations.
61 runtime: tokio::runtime::Runtime,
62
63 /// Receiving end of a `tokio::sync::oneshot` channel used to get results
64 /// back from async `DataFuture` tasks onto the UI thread.
65 pipe: Option<Receiver<ContainerResult>>,
66
67 /// Vector to keep track of active `tokio` task handles. (Mainly for potential future management)
68 tasks: Vec<tokio::task::JoinHandle<()>>,
69}
70
71impl Default for PolarsViewApp {
72 /// Creates a default `PolarsViewApp` instance. Initializes the runtime and sets initial state.
73 fn default() -> Self {
74 Self {
75 data_container: None, // No data loaded initially.
76 applied_filter: DataFilter::default(), // Start with default filter settings.
77 applied_format: DataFormat::default(), // Start with default format settings.
78 file_info: None, // No file_info initially.
79 notification: None, // No notification initially.
80 runtime: tokio::runtime::Builder::new_multi_thread() // Essential: Use a multi-threaded runtime.
81 .enable_all() // Enable necessary Tokio features (I/O, time, etc.).
82 .build()
83 .expect("Failed to build Tokio runtime"), // Runtime creation is critical.
84 pipe: None, // No async operation pending at start.
85 tasks: Vec::new(), // No tasks running at start.
86 }
87 }
88}
89
90impl PolarsViewApp {
91 /// Creates a new `PolarsViewApp` instance.
92 /// Sets the initial UI style (theme).
93 pub fn new(cc: &eframe::CreationContext<'_>) -> PolarsViewResult<Self> {
94 // Apply custom styles and dark theme (defined via `MyStyle` trait in `traits.rs`).
95 cc.egui_ctx.set_style_init(Visuals::dark());
96
97 cc.egui_ctx.memory_mut(|mem| {
98 mem.data.clear();
99 });
100
101 Ok(Default::default()) // Return a new app with default settings.
102 }
103
104 /// Creates a new `PolarsViewApp` and immediately starts loading data using a provided `DataFuture`.
105 /// Useful for loading data specified via command-line arguments on startup.
106 /// `future`: The asynchronous operation (e.g., `DataContainer::load_data`) to run.
107 pub fn new_with_future(
108 cc: &eframe::CreationContext<'_>,
109 future: DataFuture,
110 ) -> PolarsViewResult<Self> {
111 cc.egui_ctx.set_style_init(Visuals::dark()); // Apply style.
112
113 cc.egui_ctx.memory_mut(|mem| {
114 mem.data.clear();
115 });
116
117 let mut app: Self = Default::default(); // Create default app instance.
118 // Initiate the asynchronous data loading process.
119 app.run_data_future(future, &cc.egui_ctx);
120 Ok(app) // Return the app (data loading will happen in the background).
121 }
122
123 /// Checks if a `Notification` is active and renders it using `egui::Window`.
124 /// Removes the notification if its `show` method returns `false` (indicating it was closed).
125 fn check_notification(&mut self, ctx: &Context) {
126 if let Some(notification) = &mut self.notification
127 && !notification.show(ctx)
128 {
129 // If `show` returns false (window closed by user or logic),
130 // clear the notification state.
131 self.notification = None;
132 }
133 }
134
135 /// Checks the `oneshot` channel (`pipe`) for the result of a pending async data operation.
136 /// This function is called repeatedly in the `update` loop.
137 ///
138 /// Returns:
139 /// - `true`: If an operation is still pending (channel was empty).
140 /// - `false`: If a result was received (success or error) or the channel was closed.
141 fn check_data_pending(&mut self) -> bool {
142 // Take the receiver out of the Option to check it.
143 let Some(mut output) = self.pipe.take() else {
144 return false; // No receiver means no operation is pending.
145 };
146
147 // Try to receive the result without blocking.
148 match output.try_recv() {
149 // --- Result Received ---
150 Ok(data_result) => {
151 match data_result {
152 // --- Async Operation Succeeded ---
153 Ok(container) => {
154 // A new `DataContainer` was successfully produced.
155 // Update the application state:
156
157 // 1. Update `applied_filter` to match the filter *used* in the new container.
158 // This ensures the UI reflects the state of the currently displayed data.
159 self.applied_filter = container.filter.as_ref().clone();
160
161 // 2. Update `applied_format` similarly. Crucial for changes like `expand_cols`.
162 self.applied_format = container.format.as_ref().clone();
163
164 // 3. Regenerate file_info based on the new container.
165 self.file_info = FileInfo::from_container(&container);
166
167 // 4. Store the new `DataContainer`, wrapped in `Arc`.
168 self.data_container = Some(Arc::new(container));
169
170 false // Indicate loading/update is complete.
171 }
172 // --- Async Operation Failed ---
173 Err(err) => {
174 // The async task returned an error.
175 let error_message = err.to_string();
176 // Display the error in a notification window.
177 self.notification = Some(Box::new(Error {
178 message: error_message,
179 }));
180 error!("Async data operation failed: {}", err); // Log the error.
181
182 false // Indicate loading/update is complete (though failed).
183 }
184 }
185 }
186 // --- No Result Yet or Channel Closed ---
187 Err(try_recv_error) => match try_recv_error {
188 // --- Channel Empty (Operation Still Running) ---
189 TryRecvError::Empty => {
190 // The async task hasn't finished yet.
191 // Put the receiver back into the `Option` so we can check again next frame.
192 self.pipe = Some(output);
193 true // Indicate operation is still pending.
194 }
195 // --- Channel Closed (Sender Dropped Prematurely) ---
196 TryRecvError::Closed => {
197 // This indicates an issue, likely the sending task panicked or exited unexpectedly.
198 let err_msg = "Async data operation channel closed unexpectedly.".to_string();
199 self.notification = Some(Box::new(Error {
200 message: err_msg.clone(),
201 }));
202 error!("{}", err_msg); // Log the error.
203
204 false // Operation is effectively complete (failed unexpectedly).
205 }
206 },
207 }
208 }
209
210 /// Spawns a `DataFuture` onto the shared `tokio` runtime.
211 /// Sets up the `oneshot` channel to receive the result.
212 /// `future`: The async operation (boxed Future) to execute.
213 /// `ctx`: The `egui::Context` used to request repaints from the background task.
214 fn run_data_future(&mut self, future: DataFuture, ctx: &Context) {
215 // Basic cleanup: remove completed task handles (optional but good practice).
216 self.tasks.retain(|task| !task.is_finished());
217
218 // Create the single-use channel for sending the result back to the UI thread.
219 let (tx, rx) = oneshot::channel::<PolarsViewResult<DataContainer>>();
220 // Store the receiving end in `self.pipe` so `check_data_pending` can poll it.
221 self.pipe = Some(rx);
222
223 // Clone the egui context so the background task can request UI repaints.
224 let ctx_clone = ctx.clone();
225
226 // Spawn the future onto the application's Tokio runtime.
227 // The task runs in the background, managed by the runtime's thread pool.
228 let handle = self.runtime.spawn(async move {
229 // Await the completion of the provided async operation.
230 let data = future.await;
231
232 // Send the result (Ok or Err) back through the oneshot channel.
233 // Ignore the result of `send`; if it fails, the receiver (`pipe`) was dropped,
234 // which `check_data_pending` handles anyway.
235 if tx.send(data).is_err() {
236 // Log if the receiver was dropped before sending - indicates UI might have closed/restarted.
237 error!("Receiver dropped before data could be sent from async task.");
238 }
239
240 // Request a repaint of the UI thread. This is crucial to ensure the UI
241 // updates immediately after the async operation completes, especially
242 // if `check_data_pending` doesn't run in the *exact* same frame.
243 ctx_clone.request_repaint();
244 });
245
246 // Store the task handle (optional, mainly for potential future management).
247 self.tasks.push(handle);
248 }
249
250 // --- Event Handlers ---
251
252 /// Handles the "Open File" action (triggered by menu or Ctrl+O).
253 fn handle_open_file(&mut self, ctx: &Context) {
254 // Use the runtime to block on the async file dialog function.
255 // This is acceptable here as it's a direct user action expecting a pause.
256 match self.runtime.block_on(open_file()) {
257 Ok(path) => {
258 // If a path was successfully selected:
259 // Update the path in the filter state.
260 match self.applied_filter.set_path(&path) {
261 // Case 1: Path was successfully set and canonicalized.
262 Ok(()) => {
263 // Log the successful operation and the canonical path now stored.
264 tracing::debug!(
265 "Open File: Path set successfully: {:?}. Triggering load_data.",
266 self.applied_filter.absolute_path // Log the verified, absolute path
267 );
268
269 self.applied_filter.read_data_from_file = true;
270 let dc = DataContainer::default();
271
272 // Create the asynchronous future to load the data using the updated filter.
273 let future = dc.load_data(
274 self.applied_filter.clone(), // Clone state needed for the async task
275 self.applied_format.clone(), // Clone format state as well
276 );
277
278 // Run the data loading task in the background.
279 // Box::pin is necessary for futures that might be !Unpin.
280 // Box::new creates the DataFuture trait object.
281 self.run_data_future(Box::new(Box::pin(future)), ctx);
282 }
283 // Case 2: Failed to set the path (e.g., path doesn't exist, canonicalization failed).
284 Err(error) => {
285 // Log the specific error encountered.
286 tracing::error!(
287 "Open File: Failed to set or canonicalize path {:?}: {}",
288 path, // Log the original path that caused the error
289 error
290 );
291
292 // Show an error notification to the user.
293 // Format a user-friendly message including the error and the problematic path.
294 self.notification = Some(Box::new(Error {
295 message: format!(
296 "Error opening file path:\n{}\n\nPath: {}",
297 error,
298 path.display() // Use .display() for a cleaner path representation in UI
299 ),
300 }));
301 }
302 }
303 }
304 Err(PolarsViewError::FileNotFound(_)) => {
305 // User cancelled the dialog, do nothing. Log potentially?
306 tracing::debug!("File open dialog cancelled by user.");
307 }
308 Err(e) => {
309 // Other error opening the dialog itself.
310 self.notification = Some(Box::new(Error {
311 message: e.to_string(),
312 }));
313 }
314 }
315 }
316
317 /// Handles the "Save" action (Ctrl+S). Saves to the *original* file path.
318 fn handle_save_file(&mut self, ctx: &Context) {
319 // Only proceed if data is loaded.
320 if let Some(container) = &self.data_container {
321 // Clone the Arc (cheap) to pass to the async task.
322 let container_clone = container.clone();
323 // Clone context for repaint request within the task.
324 let ctx_clone = ctx.clone();
325 // Spawn the save operation onto the runtime to avoid blocking the UI.
326 self.runtime.spawn(async move {
327 if let Err(err) = save(container_clone, ctx_clone).await {
328 // Log error if saving fails. Error notification could also be added here
329 // via a channel back to the main thread if more user feedback is desired.
330 error!("Failed to save file: {}", err);
331 // TODO: Notify user of save failure via channel?
332 }
333 // Note: `save` itself now requests repaint upon completion/error.
334 });
335 }
336 }
337
338 /// Handles the "Save As..." action (Ctrl+A). Prompts user for a new location/format.
339 fn handle_save_as(&mut self, ctx: &Context) {
340 // Only proceed if data is loaded.
341 if let Some(container) = &self.data_container {
342 // Clone Arc and context for the async task.
343 let container_clone = container.clone();
344 let ctx_clone = ctx.clone();
345 // Spawn the save_as operation onto the runtime.
346 self.runtime.spawn(async move {
347 if let Err(err) = save_as(container_clone, ctx_clone).await {
348 // Log error if saving fails. Similar notification strategy as `handle_save_file` applies.
349 error!("Failed to save file using 'Save As': {}", err);
350 }
351 // Note: `save_as` itself now requests repaint upon completion/error.
352 });
353 }
354 }
355
356 // --- UI Rendering Methods ---
357
358 /// Renders the top menu bar (`TopBottomPanel`).
359 fn render_menu_bar(&mut self, ctx: &Context) {
360 TopBottomPanel::top("top_panel").show(ctx, |ui| {
361 // Use egui's built-in menu bar layout.
362 MenuBar::new().ui(ui, |ui| {
363 // Arrange menu buttons horizontally.
364 ui.horizontal(|ui| {
365 self.render_file_menu(ui); // "File" menu
366 self.render_help_menu(ui); // "Help" menu
367 // Add space and theme switch aligned to the right.
368 ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
369 self.render_theme(ui);
370 });
371 });
372 });
373 });
374 }
375
376 /// Renders the "File" menu contents.
377 fn render_file_menu(&mut self, ui: &mut egui::Ui) {
378 ui.menu_button("File", |ui| {
379 // --- File Operations ---
380 // Use a Grid for alignment of buttons and shortcuts.
381 Grid::new("file_ops_grid")
382 .num_columns(2)
383 .spacing([20.0, 10.0]) // spacing [horizontal, vertical]
384 .show(ui, |ui| {
385 // "Open File..." button
386 if ui.button("Open File...").clicked() {
387 self.handle_open_file(ui.ctx()); // Trigger the action.
388 ui.close(); // Close the menu after clicking.
389 }
390 ui.label("Ctrl + O");
391 ui.end_row();
392
393 // "Save" button (enabled only if data is loaded)
394 let save_enabled = self.data_container.is_some();
395 if ui
396 .add_enabled(save_enabled, egui::Button::new("Save"))
397 .clicked()
398 {
399 self.handle_save_file(ui.ctx());
400 ui.close();
401 }
402 ui.label("Ctrl + S");
403 ui.end_row();
404
405 // "Save As..." button (enabled only if data is loaded)
406 let save_as_enabled = self.data_container.is_some();
407 if ui
408 .add_enabled(save_as_enabled, egui::Button::new("Save As..."))
409 .clicked()
410 {
411 self.handle_save_as(ui.ctx());
412 ui.close();
413 }
414 ui.label("Ctrl + A");
415 ui.end_row();
416 });
417
418 ui.separator(); // Visual separator.
419
420 // --- Application Settings & Exit ---
421 Grid::new("app_ops_grid")
422 .num_columns(2) // Simplified for fewer items.
423 .spacing([20.0, 10.0])
424 .show(ui, |ui| {
425 // "Settings" button (Placeholder - shows a basic notification)
426 if ui.button("Settings").clicked() {
427 self.notification = Some(Box::new(Settings {})); // Show placeholder.
428 ui.close();
429 }
430 ui.label(""); // Placeholder for alignment.
431 ui.end_row();
432
433 // "Exit" button
434 if ui.button("Exit").clicked() {
435 // Send command to close the application window.
436 ui.ctx().send_viewport_cmd(ViewportCommand::Close);
437 }
438 ui.label("");
439 ui.end_row();
440 });
441 });
442 }
443
444 /// Renders the "Help" menu contents.
445 fn render_help_menu(&mut self, ui: &mut egui::Ui) {
446 ui.menu_button("Help", |ui| {
447 // Link to documentation.
448 let url = "https://docs.rs/polars-view";
449 ui.hyperlink_to("Documentation", url).on_hover_text(url);
450
451 ui.separator();
452
453 // "About" submenu.
454 ui.menu_button("About", |ui| {
455 // Display application info within a styled Frame.
456 Frame::default()
457 .stroke(Stroke::new(1.0, Color32::GRAY))
458 .outer_margin(2.0)
459 .inner_margin(10.0)
460 .show(ui, |ui| {
461 // Retrieve package info from Cargo environment variables (set at build time).
462 let version = env!("CARGO_PKG_VERSION");
463 let authors = env!("CARGO_PKG_AUTHORS");
464 let description = env!("CARGO_PKG_DESCRIPTION");
465 let name = env!("CARGO_PKG_NAME"); // Use package name for title
466
467 // Use a Grid for structured layout.
468 Grid::new("about_grid")
469 .num_columns(1) // Single column layout.
470 .spacing([10.0, 8.0]) // Tighter spacing.
471 .show(ui, |ui| {
472 ui.set_min_width(400.0); // Enforce minimum width.
473
474 // Centered Title
475 ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
476 ui.label(
477 RichText::new(name)
478 .font(FontId::proportional(28.0))
479 .strong(),
480 );
481 ui.label(
482 RichText::new(description).font(FontId::proportional(20.0)),
483 );
484 ui.label("");
485 ui.label(format!("Version: {version}"));
486 ui.label(format!("Author: {authors}"));
487 });
488 ui.end_row();
489
490 ui.separator();
491 ui.end_row();
492
493 // Links - Use horizontal layouts for label + link.
494 ui.horizontal(|ui| {
495 ui.label("Powered by");
496 let url = "https://github.com/pola-rs/polars";
497 ui.hyperlink_to("Polars", url).on_hover_text(url);
498 });
499 ui.end_row();
500
501 ui.horizontal(|ui| {
502 ui.label("Built with");
503 let url = "https://github.com/emilk/egui";
504 ui.hyperlink_to("egui", url).on_hover_text(url);
505 ui.label("&");
506 let url_eframe =
507 "https://github.com/emilk/egui/tree/master/crates/eframe";
508 ui.hyperlink_to("eframe", url_eframe)
509 .on_hover_text(url_eframe);
510 });
511 ui.end_row();
512
513 ui.horizontal(|ui| {
514 ui.label("Inspired by");
515 let url_parq = "https://github.com/Kxnr/parqbench";
516 ui.hyperlink_to("parqbench", url_parq)
517 .on_hover_text(url_parq);
518 });
519 ui.end_row();
520 });
521 });
522 });
523 });
524 }
525
526 /// Renders the Light/Dark theme selection radio buttons.
527 fn render_theme(&mut self, ui: &mut egui::Ui) {
528 // Determine current theme.
529 let mut dark_mode = ui.ctx().style().visuals.dark_mode; // Get boolean dark_mode state.
530
531 // Use radio buttons to select the theme.
532 // The `radio_value` function updates the `dark_mode` variable directly if clicked.
533 let dark_changed = ui
534 .radio_value(&mut dark_mode, true, "🌙")
535 .on_hover_text("Dark Theme")
536 .changed();
537
538 let light_changed = ui
539 .radio_value(&mut dark_mode, false, "🔆")
540 .on_hover_text("Light Theme")
541 .changed();
542
543 // If the theme selection changed, apply the new visuals.
544 if dark_changed {
545 ui.ctx().set_style_init(Visuals::dark()); // Switch to dark theme.
546 }
547
548 if light_changed {
549 ui.ctx().set_style_init(Visuals::light()); // Switch to light theme.
550 }
551 }
552
553 /// Renders the left side panel containing collapsible sections for configuration and info.
554 fn render_side_panel(&mut self, ctx: &Context) {
555 SidePanel::left("side_panel")
556 .resizable(true)
557 .default_width(300.0)
558 .show(ctx, |ui| {
559 // Use a ScrollArea in case content exceeds panel height.
560 ScrollArea::vertical().show(ui, |ui| {
561 // --- Info Section ---
562 // Only show if file_info is available.
563 if let Some(file_info) = &self.file_info {
564 ui.collapsing("Info", |ui| {
565 file_info.render_metadata(ui); // Delegate rendering to FileInfo.
566 });
567 }
568
569 // --- Format Section ---
570 ui.collapsing("Format", |ui| {
571 // Render format UI. `render_format` updates `self.applied_format`
572 // and signals if a change occurred.
573 if let Some(new_format) = self.applied_format.render_format(ui) {
574 tracing::debug!("render_side_panel: Format change detected.");
575 if let Some(data_container) = &self.data_container {
576 // Create the async future to apply the format change.
577 let future =
578 data_container.as_ref().clone().update_format(new_format);
579
580 // Schedule the async update using the standard mechanism.
581 self.run_data_future(Box::new(Box::pin(future)), ctx);
582 }
583 }
584 });
585
586 // --- Query Section ---
587 ui.collapsing("Query", |ui| {
588 // Render query UI. `render_query` updates `self.applied_filter`
589 // and signals if a change occurred triggering a load/requery.
590 if let Some(new_filter) = self.applied_filter.render_query(ui) {
591 tracing::debug!("render_side_panel: Filter change detected.");
592 if let Some(data_container) = &self.data_container {
593 // Create the async future to apply the filter change.
594 let future = data_container.as_ref().clone().load_data(
595 new_filter, // The changed filter state.
596 self.applied_format.clone(), // Keep the current format.
597 );
598 // Schedule the async reload/requery.
599 self.run_data_future(Box::new(Box::pin(future)), ctx);
600 }
601 }
602 });
603
604 // --- Columns Section ---
605 // Only show if file_info (which contains schema) is available.
606 if let Some(file_info) = &self.file_info {
607 ui.collapsing("Columns", |ui| {
608 file_info.render_schema(ui); // Delegate rendering to FileInfo.
609 });
610 }
611 });
612 });
613 }
614
615 /// Renders the bottom status bar.
616 fn render_bottom_panel(&mut self, ctx: &Context) {
617 TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| {
618 ui.horizontal(|ui| {
619 // Display file path and sort status if data is loaded
620 if let Some(container) = &self.data_container {
621 // Use lossy conversion as fallback for non-UTF8 paths
622 ui.label(format!(
623 "File: {}",
624 container.filter.absolute_path.to_string_lossy()
625 ));
626 ui.separator();
627 // Show how many columns are involved in the sort
628 ui.label(format!("Sort: {} active criteria", container.sort.len()));
629 } else {
630 ui.label("No file loaded."); // Default message
631 }
632
633 // Show spinner and text if an async operation is pending
634 if self.pipe.is_some() {
635 ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
636 ui.spinner();
637 ui.label("Processing... "); // Indicate background work
638 });
639 }
640 });
641 });
642 }
643
644 /// Renders the central panel, primarily displaying the data table.
645 /// Handles triggering the `apply_sort` async operation based on header clicks.
646 fn render_central_panel(&mut self, ctx: &Context) {
647 CentralPanel::default().show(ctx, |ui| {
648 egui::warn_if_debug_build(ui); // Debug build reminder overlay
649
650 // Check async task status BEFORE rendering UI potentially disabled by it
651 let is_pending = self.check_data_pending();
652
653 // Disable central panel interaction while loading/sorting/etc.
654 ui.add_enabled_ui(!is_pending, |ui| {
655 match &self.data_container {
656 Some(data_container) => {
657 // Variable to capture the new sort criteria requested by header clicks
658 let mut opt_new_sort_criteria: Option<Vec<SortBy>> = None;
659
660 // Use horizontal scroll area for wide tables
661 ScrollArea::horizontal()
662 .id_salt("central_scroll") // Add ID for state persistence
663 .auto_shrink([false, false]) // Don't shrink if content fits
664 .show(ui, |ui| {
665 // `render_table` handles drawing and captures header interactions.
666 // Returns `Some(Vec<SortBy>)` if a click occurred
667 // that modified the sort order (add, remove, change direction).
668 opt_new_sort_criteria = data_container.render_table(ui);
669 });
670
671 // --- Handle Sort Action Triggered from Table Header ---
672 if let Some(new_criteria) = opt_new_sort_criteria {
673 // A header click signaled a request to change the sort state.
674 tracing::debug!(
675 "render_central_panel: Sort action requested by header click. Triggering apply_sort. New criteria: {:#?}",
676 new_criteria // Log the requested criteria
677 );
678
679 // Trigger the `apply_sort` async operation.
680 // This handles both applying new sorts and resetting (if new_criteria is empty).
681 let future = data_container.as_ref().clone().apply_sort(
682 new_criteria, // Pass the new requested sort criteria Vec
683 );
684 self.run_data_future(Box::new(Box::pin(future)), ctx);
685 }
686 }
687 None => { // No data container is loaded
688 ui.centered_and_justified(|ui| {
689 if is_pending {
690 ui.spinner(); // Show loading indicator if async task is running
691 } else {
692 // Show help message if idle and no data
693 ui.label("Open a file (File > Open or Ctrl+O) or drag & drop CSV, JSON, or Parquet files here.");
694 }
695 });
696 }
697 }
698 }); // End of add_enabled_ui block
699 }); // End CentralPanel
700 }
701}
702
703// --- eframe::App Implementation ---
704
705impl eframe::App for PolarsViewApp {
706 /// The main update function called by `eframe` on each frame (the "render loop").
707 /// Responsible for handling events, updating state, and drawing the UI.
708 fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
709 // 1. Check and display any active Notification windows (errors, etc.).
710 self.check_notification(ctx);
711
712 // 2. Handle file drag-and-drop events.
713 // Check if any files were dropped onto the window in this frame.
714 // Take the first dropped file only
715 if let Some(dropped_file) = ctx.input(|i| i.raw.dropped_files.first().cloned()) {
716 if let Some(path) = &dropped_file.path {
717 // Attempt to set the path in the filter state, handling potential errors.
718 match self.applied_filter.set_path(path) {
719 // Case 1: Path was successfully set and canonicalized.
720 Ok(()) => {
721 // Log the successful operation and the canonical path now stored.
722 tracing::debug!(
723 "Drag-n-drop: Path set successfully: {:?}. Triggering load_data.",
724 self.applied_filter.absolute_path // Log the verified, absolute path
725 );
726
727 // Create the asynchronous future to load the data using the updated filter.
728 let dc = DataContainer::default();
729 let future = dc.load_data(
730 self.applied_filter.clone(), // Clone state needed for the async task
731 self.applied_format.clone(), // Clone format state as well
732 );
733
734 // Run the data loading task in the background.
735 // Box::pin is necessary for futures that might be !Unpin.
736 // Box::new creates the DataFuture trait object.
737 self.run_data_future(Box::new(Box::pin(future)), ctx);
738 }
739 // Case 2: Failed to set the path (e.g., path doesn't exist, canonicalization failed).
740 Err(error) => {
741 // Log the specific error encountered.
742 tracing::error!(
743 "Drag-n-drop: Failed to set or canonicalize path {:?}: {}",
744 path, // Log the original path that caused the error
745 error
746 );
747
748 // Show an error notification to the user.
749 // Format a user-friendly message including the error and the problematic path.
750 self.notification = Some(Box::new(Error {
751 message: format!(
752 "Error processing dropped file path:\n{}\n\nPath: {}",
753 error,
754 path.display() // Use .display() for a cleaner path representation in UI
755 ),
756 }));
757 }
758 }
759 } else {
760 // Optional: Log if a dropped item lacked a path.
761 tracing::warn!(
762 "Drag-n-drop: Ignored dropped item without a path: {:?}",
763 dropped_file
764 );
765 }
766 }
767
768 // 3. Handle global keyboard shortcuts *before* drawing UI elements that might consume input.
769 ctx.input_mut(|i| {
770 if i.consume_shortcut(&CTRL_O) {
771 // Open File
772 self.handle_open_file(ctx);
773 }
774 if i.consume_shortcut(&CTRL_S) {
775 // Save File
776 self.handle_save_file(ctx);
777 }
778 if i.consume_shortcut(&CTRL_A) {
779 // Save As...
780 self.handle_save_as(ctx);
781 }
782 });
783
784 // 4. Define the main UI layout using `egui` panels.
785 // The order matters: Top/Bottom/Left/Right panels are defined *before* the CentralPanel.
786
787 // 4a. Top panel for the menu bar.
788 self.render_menu_bar(ctx);
789
790 // 4b. Left side panel for configuration (Filters, Format) and info (Info, Columns).
791 self.render_side_panel(ctx);
792
793 // 4c. Bottom panel for displaying status info (e.g., loaded file path).
794 self.render_bottom_panel(ctx);
795
796 // 4d. Central panel: The main content area, primarily for the data table.
797 // Must be added *last*.
798 self.render_central_panel(ctx);
799 }
800}