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}