1use std::path::PathBuf;
2
3use crate::core::{DialogMode, FileDialogError, LayoutStyle, Selection};
4use crate::custom_pane::{CustomPane, CustomPaneCtx};
5use crate::dialog_core::{ConfirmGate, CoreEvent};
6use crate::dialog_state::CustomPaneDock;
7use crate::dialog_state::{FileDialogState, FileListViewMode};
8use crate::fs::{FileSystem, StdFileSystem};
9use crate::thumbnails::ThumbnailBackend;
10use dear_imgui_rs::Ui;
11use dear_imgui_rs::input::MouseCursor;
12use dear_imgui_rs::sys;
13
14mod file_table;
15mod footer;
16mod header;
17mod igfd_path_popup;
18mod ops;
19mod path_bar;
20mod places;
21mod popups;
22
23#[derive(Clone, Debug)]
25pub struct WindowHostConfig {
26 pub title: String,
28 pub initial_size: [f32; 2],
30 pub size_condition: dear_imgui_rs::Condition,
32 pub min_size: Option<[f32; 2]>,
34 pub max_size: Option<[f32; 2]>,
36}
37
38impl WindowHostConfig {
39 pub fn for_mode(mode: DialogMode) -> Self {
41 let title = match mode {
42 DialogMode::OpenFile | DialogMode::OpenFiles => "Open",
43 DialogMode::PickFolder => "Select Folder",
44 DialogMode::SaveFile => "Save",
45 };
46 Self {
47 title: title.to_string(),
48 initial_size: [760.0, 520.0],
49 size_condition: dear_imgui_rs::Condition::FirstUseEver,
50 min_size: None,
51 max_size: None,
52 }
53 }
54}
55
56#[derive(Clone, Debug)]
61pub struct ModalHostConfig {
62 pub popup_label: String,
64 pub initial_size: [f32; 2],
66 pub size_condition: dear_imgui_rs::Condition,
68 pub min_size: Option<[f32; 2]>,
70 pub max_size: Option<[f32; 2]>,
72}
73
74impl ModalHostConfig {
75 pub fn for_mode(mode: DialogMode) -> Self {
77 let title = match mode {
78 DialogMode::OpenFile | DialogMode::OpenFiles => "Open",
79 DialogMode::PickFolder => "Select Folder",
80 DialogMode::SaveFile => "Save",
81 };
82 Self {
83 popup_label: format!("{title}###FileBrowserModal"),
84 initial_size: [760.0, 520.0],
85 size_condition: dear_imgui_rs::Condition::FirstUseEver,
86 min_size: None,
87 max_size: None,
88 }
89 }
90}
91
92pub struct FileBrowser<'ui> {
94 pub ui: &'ui Ui,
95}
96
97pub trait FileDialogExt {
99 fn file_browser(&self) -> FileBrowser<'_>;
101}
102
103impl FileDialogExt for Ui {
104 fn file_browser(&self) -> FileBrowser<'_> {
105 FileBrowser { ui: self }
106 }
107}
108
109impl<'ui> FileBrowser<'ui> {
110 pub fn draw_contents(
117 &self,
118 state: &mut FileDialogState,
119 ) -> Option<Result<Selection, FileDialogError>> {
120 self.draw_contents_with(state, &StdFileSystem, None, None)
121 }
122
123 pub fn draw_contents_with(
129 &self,
130 state: &mut FileDialogState,
131 fs: &dyn FileSystem,
132 mut custom_pane: Option<&mut dyn CustomPane>,
133 mut thumbnails_backend: Option<&mut ThumbnailBackend<'_>>,
134 ) -> Option<Result<Selection, FileDialogError>> {
135 draw_contents_with_fs_and_hooks(
136 self.ui,
137 state,
138 fs,
139 custom_pane.take(),
140 thumbnails_backend.take(),
141 )
142 }
143
144 pub fn show(&self, state: &mut FileDialogState) -> Option<Result<Selection, FileDialogError>> {
147 let cfg = WindowHostConfig::for_mode(state.core.mode);
148 self.show_windowed(state, &cfg)
149 }
150
151 pub fn show_windowed(
154 &self,
155 state: &mut FileDialogState,
156 cfg: &WindowHostConfig,
157 ) -> Option<Result<Selection, FileDialogError>> {
158 self.show_windowed_with(state, cfg, &StdFileSystem, None, None)
159 }
160
161 pub fn show_windowed_with(
163 &self,
164 state: &mut FileDialogState,
165 cfg: &WindowHostConfig,
166 fs: &dyn FileSystem,
167 mut custom_pane: Option<&mut dyn CustomPane>,
168 mut thumbnails_backend: Option<&mut ThumbnailBackend<'_>>,
169 ) -> Option<Result<Selection, FileDialogError>> {
170 if !state.ui.visible {
171 return None;
172 }
173
174 let mut out: Option<Result<Selection, FileDialogError>> = None;
175 let mut window = self
176 .ui
177 .window(&cfg.title)
178 .size(cfg.initial_size, cfg.size_condition);
179 if let Some((min_size, max_size)) =
180 resolve_host_size_constraints(cfg.min_size, cfg.max_size)
181 {
182 window = window.size_constraints(min_size, max_size);
183 }
184 window.build(|| {
185 out = draw_contents_with_fs_and_hooks(
186 self.ui,
187 state,
188 fs,
189 custom_pane.take(),
190 thumbnails_backend.take(),
191 );
192 });
193 out
194 }
195
196 pub fn show_modal(
199 &self,
200 state: &mut FileDialogState,
201 ) -> Option<Result<Selection, FileDialogError>> {
202 let cfg = ModalHostConfig::for_mode(state.core.mode);
203 self.show_modal_with(state, &cfg, &StdFileSystem, None, None)
204 }
205
206 pub fn show_modal_with(
208 &self,
209 state: &mut FileDialogState,
210 cfg: &ModalHostConfig,
211 fs: &dyn FileSystem,
212 mut custom_pane: Option<&mut dyn CustomPane>,
213 mut thumbnails_backend: Option<&mut ThumbnailBackend<'_>>,
214 ) -> Option<Result<Selection, FileDialogError>> {
215 if !state.ui.visible {
216 return None;
217 }
218
219 if !self.ui.is_popup_open(&cfg.popup_label) {
220 self.ui.open_popup(&cfg.popup_label);
221 }
222
223 if let Some((min_size, max_size)) =
224 resolve_host_size_constraints(cfg.min_size, cfg.max_size)
225 {
226 unsafe {
227 let min_vec = sys::ImVec2_c {
228 x: min_size[0],
229 y: min_size[1],
230 };
231 let max_vec = sys::ImVec2_c {
232 x: max_size[0],
233 y: max_size[1],
234 };
235 sys::igSetNextWindowSizeConstraints(min_vec, max_vec, None, std::ptr::null_mut());
236 }
237 }
238
239 unsafe {
240 let size_vec = sys::ImVec2 {
241 x: cfg.initial_size[0],
242 y: cfg.initial_size[1],
243 };
244 sys::igSetNextWindowSize(size_vec, cfg.size_condition as i32);
245 }
246
247 let Some(_popup) = self.ui.begin_modal_popup(&cfg.popup_label) else {
248 return None;
249 };
250
251 let out = draw_contents_with_fs_and_hooks(
252 self.ui,
253 state,
254 fs,
255 custom_pane.take(),
256 thumbnails_backend.take(),
257 );
258 if out.is_some() {
259 self.ui.close_current_popup();
260 }
261 out
262 }
263}
264
265fn resolve_host_size_constraints(
266 min_size: Option<[f32; 2]>,
267 max_size: Option<[f32; 2]>,
268) -> Option<([f32; 2], [f32; 2])> {
269 if min_size.is_none() && max_size.is_none() {
270 return None;
271 }
272
273 let sanitize = |value: f32, fallback: f32| -> f32 {
274 if value.is_finite() {
275 value.max(0.0)
276 } else {
277 fallback
278 }
279 };
280
281 let mut min = min_size.unwrap_or([0.0, 0.0]);
282 min[0] = sanitize(min[0], 0.0);
283 min[1] = sanitize(min[1], 0.0);
284
285 let mut max = max_size.unwrap_or([f32::MAX, f32::MAX]);
286 max[0] = sanitize(max[0], f32::MAX);
287 max[1] = sanitize(max[1], f32::MAX);
288
289 max[0] = max[0].max(min[0]);
290 max[1] = max[1].max(min[1]);
291
292 Some((min, max))
293}
294
295fn draw_contents_with_fs_and_hooks(
296 ui: &Ui,
297 state: &mut FileDialogState,
298 fs: &dyn FileSystem,
299 mut custom_pane: Option<&mut dyn CustomPane>,
300 mut thumbnails_backend: Option<&mut ThumbnailBackend<'_>>,
301) -> Option<Result<Selection, FileDialogError>> {
302 if !state.ui.visible {
303 return None;
304 }
305
306 let _dialog_id_scope = ui.push_id(state as *mut FileDialogState);
310
311 let has_thumbnail_backend = thumbnails_backend.is_some();
312 let mut request_confirm = false;
313 let mut confirm_gate = ConfirmGate::default();
314
315 header::draw_chrome(ui, state, fs, has_thumbnail_backend);
316
317 let avail = ui.content_region_avail();
319 let footer_h = state
320 .ui
321 .runtime
322 .footer
323 .height_last
324 .max(footer::estimate_footer_height(ui, state));
325 let content_h = (avail[1] - footer_h).max(0.0);
326 match state.ui.config.layout {
327 LayoutStyle::Standard => {
328 if state.ui.config.places_pane_shown {
329 const MIN_PLACES_W: f32 = 120.0;
330 const MIN_FILE_LIST_W: f32 = 180.0;
331
332 let splitter_w = splitter_width(ui);
333 let spacing_x = ui.clone_style().item_spacing()[0];
334 let max_places_w =
335 (avail[0] - MIN_FILE_LIST_W - splitter_w - spacing_x * 2.0).max(0.0);
336 let mut places_w = state.ui.config.places_pane_width.clamp(0.0, max_places_w);
337 if max_places_w >= MIN_PLACES_W {
338 places_w = places_w.clamp(MIN_PLACES_W, max_places_w);
339 }
340 let file_w = (avail[0] - places_w - splitter_w - spacing_x * 2.0).max(0.0);
341
342 let mut new_cwd: Option<PathBuf> = None;
343 ui.child_window("places_pane")
344 .size([places_w, content_h])
345 .border(true)
346 .build(ui, || {
347 new_cwd = places::draw_places_pane(ui, state);
348 });
349 if let Some(p) = new_cwd {
350 let _ = state.core.handle_event(CoreEvent::NavigateTo(p));
351 }
352
353 ui.same_line();
354 ui.invisible_button("places_pane_splitter", [splitter_w, content_h]);
355 if ui.is_item_hovered() || ui.is_item_active() {
356 ui.set_mouse_cursor(Some(MouseCursor::ResizeEW));
357 }
358 if ui.is_item_active() {
359 let dx = ui.io().mouse_delta()[0];
360 let new_w = (places_w + dx).clamp(0.0, max_places_w);
361 state.ui.config.places_pane_width = if max_places_w >= MIN_PLACES_W {
362 new_w.clamp(MIN_PLACES_W, max_places_w)
363 } else {
364 new_w
365 };
366 }
367
368 ui.same_line();
369 ui.child_window("file_list")
370 .size([file_w, content_h])
371 .build(ui, || {
372 let inner = ui.content_region_avail();
373 let show_pane = state.ui.config.custom_pane_enabled
374 && custom_pane.as_deref_mut().is_some();
375 if !show_pane {
376 file_table::draw_file_table(
377 ui,
378 state,
379 [inner[0], inner[1]],
380 fs,
381 &mut request_confirm,
382 thumbnails_backend.as_deref_mut(),
383 );
384 return;
385 }
386
387 match state.ui.config.custom_pane_dock {
388 CustomPaneDock::Bottom => {
389 let style = ui.clone_style();
390 let sep_h = style.item_spacing()[1] * 2.0 + 1.0;
391 let pane_h = state
392 .ui
393 .config
394 .custom_pane_height
395 .clamp(0.0, inner[1].max(0.0));
396 let mut table_h = inner[1];
397 if pane_h > 0.0 {
398 table_h = (table_h - pane_h - sep_h).max(0.0);
399 }
400
401 file_table::draw_file_table(
402 ui,
403 state,
404 [inner[0], table_h],
405 fs,
406 &mut request_confirm,
407 thumbnails_backend.as_deref_mut(),
408 );
409
410 if let Some(pane) = custom_pane.as_deref_mut() {
411 if state.ui.config.custom_pane_enabled && pane_h > 0.0 {
412 ui.separator();
413 ui.child_window("custom_pane")
414 .size([inner[0], pane_h])
415 .border(true)
416 .build(ui, || {
417 let selected_entry_ids =
418 state.core.selected_entry_ids();
419 let selected_paths =
420 ops::selected_entry_paths_from_ids(state);
421 let (selected_files_count, selected_dirs_count) =
422 ops::selected_entry_counts_from_ids(state);
423 let ctx = CustomPaneCtx {
424 mode: state.core.mode,
425 cwd: &state.core.cwd,
426 selected_entry_ids: &selected_entry_ids,
427 selected_paths: &selected_paths,
428 selected_files_count,
429 selected_dirs_count,
430 save_name: &state.core.save_name,
431 active_filter: state.core.active_filter(),
432 };
433 confirm_gate = pane.draw(ui, ctx);
434 });
435 }
436 }
437 }
438 CustomPaneDock::Right => {
439 const MIN_TABLE_W: f32 = 120.0;
440 const MIN_PANE_W: f32 = 120.0;
441
442 let splitter_w = splitter_width(ui);
443 let max_pane_w = (inner[0] - MIN_TABLE_W - splitter_w).max(0.0);
444 let mut pane_w =
445 state.ui.config.custom_pane_width.clamp(0.0, max_pane_w);
446 if max_pane_w >= MIN_PANE_W {
447 pane_w = pane_w.clamp(MIN_PANE_W, max_pane_w);
448 }
449
450 let table_w = (inner[0] - pane_w - splitter_w).max(0.0);
451
452 ui.child_window("file_table_rightdock")
453 .size([table_w, inner[1]])
454 .build(ui, || {
455 file_table::draw_file_table(
456 ui,
457 state,
458 [table_w, inner[1]],
459 fs,
460 &mut request_confirm,
461 thumbnails_backend.as_deref_mut(),
462 );
463 });
464
465 ui.same_line();
466 ui.invisible_button("custom_pane_splitter", [splitter_w, inner[1]]);
467 if ui.is_item_hovered() || ui.is_item_active() {
468 ui.set_mouse_cursor(Some(MouseCursor::ResizeEW));
469 }
470 if ui.is_item_active() {
471 let dx = ui.io().mouse_delta()[0];
472 let new_w = (pane_w - dx).clamp(0.0, max_pane_w);
473 state.ui.config.custom_pane_width = if max_pane_w >= MIN_PANE_W
474 {
475 new_w.clamp(MIN_PANE_W, max_pane_w)
476 } else {
477 new_w
478 };
479 }
480
481 ui.same_line();
482 ui.child_window("custom_pane_rightdock")
483 .size([pane_w, inner[1]])
484 .border(true)
485 .build(ui, || {
486 if let Some(pane) = custom_pane.as_deref_mut() {
487 let selected_entry_ids =
488 state.core.selected_entry_ids();
489 let selected_paths =
490 ops::selected_entry_paths_from_ids(state);
491 let (selected_files_count, selected_dirs_count) =
492 ops::selected_entry_counts_from_ids(state);
493 let ctx = CustomPaneCtx {
494 mode: state.core.mode,
495 cwd: &state.core.cwd,
496 selected_entry_ids: &selected_entry_ids,
497 selected_paths: &selected_paths,
498 selected_files_count,
499 selected_dirs_count,
500 save_name: &state.core.save_name,
501 active_filter: state.core.active_filter(),
502 };
503 confirm_gate = pane.draw(ui, ctx);
504 }
505 });
506 }
507 }
508 });
509 } else {
510 ui.child_window("file_list")
511 .size([avail[0], content_h])
512 .build(ui, || {
513 let inner = ui.content_region_avail();
514 let show_pane = state.ui.config.custom_pane_enabled
515 && custom_pane.as_deref_mut().is_some();
516 if !show_pane {
517 file_table::draw_file_table(
518 ui,
519 state,
520 [inner[0], inner[1]],
521 fs,
522 &mut request_confirm,
523 thumbnails_backend.as_deref_mut(),
524 );
525 return;
526 }
527
528 match state.ui.config.custom_pane_dock {
529 CustomPaneDock::Bottom => {
530 let style = ui.clone_style();
531 let sep_h = style.item_spacing()[1] * 2.0 + 1.0;
532 let pane_h = state
533 .ui
534 .config
535 .custom_pane_height
536 .clamp(0.0, inner[1].max(0.0));
537 let mut table_h = inner[1];
538 if pane_h > 0.0 {
539 table_h = (table_h - pane_h - sep_h).max(0.0);
540 }
541
542 file_table::draw_file_table(
543 ui,
544 state,
545 [inner[0], table_h],
546 fs,
547 &mut request_confirm,
548 thumbnails_backend.as_deref_mut(),
549 );
550
551 if let Some(pane) = custom_pane.as_deref_mut() {
552 if state.ui.config.custom_pane_enabled && pane_h > 0.0 {
553 ui.separator();
554 ui.child_window("custom_pane")
555 .size([inner[0], pane_h])
556 .border(true)
557 .build(ui, || {
558 let selected_entry_ids =
559 state.core.selected_entry_ids();
560 let selected_paths =
561 ops::selected_entry_paths_from_ids(state);
562 let (selected_files_count, selected_dirs_count) =
563 ops::selected_entry_counts_from_ids(state);
564 let ctx = CustomPaneCtx {
565 mode: state.core.mode,
566 cwd: &state.core.cwd,
567 selected_entry_ids: &selected_entry_ids,
568 selected_paths: &selected_paths,
569 selected_files_count,
570 selected_dirs_count,
571 save_name: &state.core.save_name,
572 active_filter: state.core.active_filter(),
573 };
574 confirm_gate = pane.draw(ui, ctx);
575 });
576 }
577 }
578 }
579 CustomPaneDock::Right => {
580 const MIN_TABLE_W: f32 = 120.0;
581 const MIN_PANE_W: f32 = 120.0;
582
583 let splitter_w = splitter_width(ui);
584 let max_pane_w = (inner[0] - MIN_TABLE_W - splitter_w).max(0.0);
585 let mut pane_w =
586 state.ui.config.custom_pane_width.clamp(0.0, max_pane_w);
587 if max_pane_w >= MIN_PANE_W {
588 pane_w = pane_w.clamp(MIN_PANE_W, max_pane_w);
589 }
590
591 let table_w = (inner[0] - pane_w - splitter_w).max(0.0);
592
593 ui.child_window("file_table_rightdock")
594 .size([table_w, inner[1]])
595 .build(ui, || {
596 file_table::draw_file_table(
597 ui,
598 state,
599 [table_w, inner[1]],
600 fs,
601 &mut request_confirm,
602 thumbnails_backend.as_deref_mut(),
603 );
604 });
605
606 ui.same_line();
607 ui.invisible_button("custom_pane_splitter", [splitter_w, inner[1]]);
608 if ui.is_item_hovered() || ui.is_item_active() {
609 ui.set_mouse_cursor(Some(MouseCursor::ResizeEW));
610 }
611 if ui.is_item_active() {
612 let dx = ui.io().mouse_delta()[0];
613 let new_w = (pane_w - dx).clamp(0.0, max_pane_w);
614 state.ui.config.custom_pane_width = if max_pane_w >= MIN_PANE_W
615 {
616 new_w.clamp(MIN_PANE_W, max_pane_w)
617 } else {
618 new_w
619 };
620 }
621
622 ui.same_line();
623 ui.child_window("custom_pane_rightdock")
624 .size([pane_w, inner[1]])
625 .border(true)
626 .build(ui, || {
627 if let Some(pane) = custom_pane.as_deref_mut() {
628 let selected_entry_ids =
629 state.core.selected_entry_ids();
630 let selected_paths =
631 ops::selected_entry_paths_from_ids(state);
632 let (selected_files_count, selected_dirs_count) =
633 ops::selected_entry_counts_from_ids(state);
634 let ctx = CustomPaneCtx {
635 mode: state.core.mode,
636 cwd: &state.core.cwd,
637 selected_entry_ids: &selected_entry_ids,
638 selected_paths: &selected_paths,
639 selected_files_count,
640 selected_dirs_count,
641 save_name: &state.core.save_name,
642 active_filter: state.core.active_filter(),
643 };
644 confirm_gate = pane.draw(ui, ctx);
645 }
646 });
647 }
648 }
649 });
650 }
651 }
652 LayoutStyle::Minimal => {
653 ui.child_window("file_list_min")
654 .size([avail[0], content_h])
655 .build(ui, || {
656 let inner = ui.content_region_avail();
657 let show_pane =
658 state.ui.config.custom_pane_enabled && custom_pane.as_deref_mut().is_some();
659 if !show_pane {
660 file_table::draw_file_table(
661 ui,
662 state,
663 [inner[0], inner[1]],
664 fs,
665 &mut request_confirm,
666 thumbnails_backend.as_deref_mut(),
667 );
668 return;
669 }
670
671 match state.ui.config.custom_pane_dock {
672 CustomPaneDock::Bottom => {
673 let style = ui.clone_style();
674 let sep_h = style.item_spacing()[1] * 2.0 + 1.0;
675 let pane_h = state
676 .ui
677 .config
678 .custom_pane_height
679 .clamp(0.0, inner[1].max(0.0));
680 let mut table_h = inner[1];
681 if pane_h > 0.0 {
682 table_h = (table_h - pane_h - sep_h).max(0.0);
683 }
684
685 file_table::draw_file_table(
686 ui,
687 state,
688 [inner[0], table_h],
689 fs,
690 &mut request_confirm,
691 thumbnails_backend.as_deref_mut(),
692 );
693
694 if let Some(pane) = custom_pane.as_deref_mut() {
695 if state.ui.config.custom_pane_enabled && pane_h > 0.0 {
696 ui.separator();
697 ui.child_window("custom_pane")
698 .size([inner[0], pane_h])
699 .border(true)
700 .build(ui, || {
701 let selected_entry_ids =
702 state.core.selected_entry_ids();
703 let selected_paths =
704 ops::selected_entry_paths_from_ids(state);
705 let (selected_files_count, selected_dirs_count) =
706 ops::selected_entry_counts_from_ids(state);
707 let ctx = CustomPaneCtx {
708 mode: state.core.mode,
709 cwd: &state.core.cwd,
710 selected_entry_ids: &selected_entry_ids,
711 selected_paths: &selected_paths,
712 selected_files_count,
713 selected_dirs_count,
714 save_name: &state.core.save_name,
715 active_filter: state.core.active_filter(),
716 };
717 confirm_gate = pane.draw(ui, ctx);
718 });
719 }
720 }
721 }
722 CustomPaneDock::Right => {
723 const MIN_TABLE_W: f32 = 120.0;
724 const MIN_PANE_W: f32 = 120.0;
725
726 let splitter_w = splitter_width(ui);
727 let max_pane_w = (inner[0] - MIN_TABLE_W - splitter_w).max(0.0);
728 let mut pane_w =
729 state.ui.config.custom_pane_width.clamp(0.0, max_pane_w);
730 if max_pane_w >= MIN_PANE_W {
731 pane_w = pane_w.clamp(MIN_PANE_W, max_pane_w);
732 }
733
734 let table_w = (inner[0] - pane_w - splitter_w).max(0.0);
735
736 ui.child_window("file_table_rightdock")
737 .size([table_w, inner[1]])
738 .build(ui, || {
739 file_table::draw_file_table(
740 ui,
741 state,
742 [table_w, inner[1]],
743 fs,
744 &mut request_confirm,
745 thumbnails_backend.as_deref_mut(),
746 );
747 });
748
749 ui.same_line();
750 ui.invisible_button("custom_pane_splitter", [splitter_w, inner[1]]);
751 if ui.is_item_hovered() || ui.is_item_active() {
752 ui.set_mouse_cursor(Some(MouseCursor::ResizeEW));
753 }
754 if ui.is_item_active() {
755 let dx = ui.io().mouse_delta()[0];
756 let new_w = (pane_w - dx).clamp(0.0, max_pane_w);
757 state.ui.config.custom_pane_width = if max_pane_w >= MIN_PANE_W {
758 new_w.clamp(MIN_PANE_W, max_pane_w)
759 } else {
760 new_w
761 };
762 }
763
764 ui.same_line();
765 ui.child_window("custom_pane_rightdock")
766 .size([pane_w, inner[1]])
767 .border(true)
768 .build(ui, || {
769 if let Some(pane) = custom_pane.as_deref_mut() {
770 let selected_entry_ids = state.core.selected_entry_ids();
771 let selected_paths =
772 ops::selected_entry_paths_from_ids(state);
773 let (selected_files_count, selected_dirs_count) =
774 ops::selected_entry_counts_from_ids(state);
775 let ctx = CustomPaneCtx {
776 mode: state.core.mode,
777 cwd: &state.core.cwd,
778 selected_entry_ids: &selected_entry_ids,
779 selected_paths: &selected_paths,
780 selected_files_count,
781 selected_dirs_count,
782 save_name: &state.core.save_name,
783 active_filter: state.core.active_filter(),
784 };
785 confirm_gate = pane.draw(ui, ctx);
786 }
787 });
788 }
789 }
790 });
791 }
792 }
793
794 if let Some(p) = igfd_path_popup::draw_igfd_path_popup(ui, state, fs, [avail[0], content_h]) {
796 let _ = state.core.handle_event(CoreEvent::NavigateTo(p));
797 }
798
799 places::draw_minimal_places_popup(ui, state);
800 popups::draw_columns_popup(ui, state, has_thumbnail_backend);
801 popups::draw_options_popup(ui, state, has_thumbnail_backend);
802
803 places::draw_places_io_modal(ui, state);
804 places::draw_places_edit_modal(ui, state, fs);
805 popups::draw_new_folder_modal(ui, state, fs);
806 popups::draw_rename_modal(ui, state, fs);
807 popups::draw_delete_confirm_modal(ui, state, fs);
808 popups::draw_paste_conflict_modal(ui, state, fs);
809
810 footer::draw_footer(ui, state, fs, &confirm_gate, &mut request_confirm);
811
812 let out = state.core.take_result();
813 if out.is_some() {
814 state.close();
815 }
816 out
817}
818
819fn splitter_width(ui: &Ui) -> f32 {
820 let w = ui.frame_height() * 0.25;
822 w.clamp(4.0, 10.0)
823}
824
825pub(in crate::ui) fn apply_file_list_view_from_ui(
826 state: &mut FileDialogState,
827 view: FileListViewMode,
828 has_thumbnail_backend: bool,
829) -> bool {
830 match view {
831 FileListViewMode::List => {
832 state.ui.config.file_list_view = FileListViewMode::List;
833 true
834 }
835 FileListViewMode::ThumbnailsList => {
836 if !has_thumbnail_backend {
837 return false;
838 }
839 state.ui.config.file_list_view = FileListViewMode::ThumbnailsList;
840 state.ui.config.thumbnails_enabled = true;
841 state.ui.config.file_list_columns.show_preview = true;
842 true
843 }
844 FileListViewMode::Grid => {
845 if !has_thumbnail_backend {
846 return false;
847 }
848 state.ui.config.file_list_view = FileListViewMode::Grid;
849 state.ui.config.thumbnails_enabled = true;
850 true
851 }
852 }
853}
854
855#[cfg(test)]
856mod tests {
857 use super::file_table::{ListColumnLayout, list_column_layout, merged_order_with_current};
858 use super::ops::{open_delete_modal_from_selection, open_rename_modal_from_selection};
859 use super::{apply_file_list_view_from_ui, resolve_host_size_constraints};
860 use crate::core::DialogMode;
861 use crate::dialog_core::EntryId;
862 use crate::dialog_state::{
863 FileDialogState, FileListColumnWeightOverrides, FileListColumnsConfig, FileListDataColumn,
864 FileListViewMode,
865 };
866 use crate::fs::{FileSystem, FsEntry, FsMetadata};
867 use dear_imgui_rs::TableColumnIndex;
868 use std::path::{Path, PathBuf};
869
870 fn columns_config(
871 show_size: bool,
872 show_modified: bool,
873 order: [FileListDataColumn; 4],
874 ) -> FileListColumnsConfig {
875 FileListColumnsConfig {
876 show_size,
877 show_modified,
878 order,
879 ..FileListColumnsConfig::default()
880 }
881 }
882
883 #[test]
884 fn resolve_host_size_constraints_returns_none_when_unset() {
885 assert!(resolve_host_size_constraints(None, None).is_none());
886 }
887
888 #[test]
889 fn resolve_host_size_constraints_supports_one_sided_values() {
890 let (min, max) = resolve_host_size_constraints(Some([200.0, 150.0]), None).unwrap();
891 assert_eq!(min, [200.0, 150.0]);
892 assert_eq!(max, [f32::MAX, f32::MAX]);
893
894 let (min, max) = resolve_host_size_constraints(None, Some([900.0, 700.0])).unwrap();
895 assert_eq!(min, [0.0, 0.0]);
896 assert_eq!(max, [900.0, 700.0]);
897 }
898
899 #[test]
900 fn resolve_host_size_constraints_normalizes_invalid_values() {
901 let (min, max) =
902 resolve_host_size_constraints(Some([300.0, f32::NAN]), Some([100.0, f32::INFINITY]))
903 .unwrap();
904 assert_eq!(min, [300.0, 0.0]);
905 assert_eq!(max, [300.0, f32::MAX]);
906 }
907
908 #[derive(Clone, Default)]
909 struct UiTestFs {
910 entries: Vec<FsEntry>,
911 }
912
913 impl FileSystem for UiTestFs {
914 fn read_dir(&self, _dir: &Path) -> std::io::Result<Vec<FsEntry>> {
915 Ok(self.entries.clone())
916 }
917
918 fn canonicalize(&self, path: &Path) -> std::io::Result<PathBuf> {
919 Ok(path.to_path_buf())
920 }
921
922 fn metadata(&self, path: &Path) -> std::io::Result<FsMetadata> {
923 self.entries
924 .iter()
925 .find(|entry| entry.path == path)
926 .map(|entry| FsMetadata {
927 is_dir: entry.is_dir,
928 is_symlink: entry.is_symlink,
929 })
930 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "not found"))
931 }
932
933 fn create_dir(&self, _path: &Path) -> std::io::Result<()> {
934 Err(std::io::Error::new(
935 std::io::ErrorKind::Unsupported,
936 "create_dir not supported in UiTestFs",
937 ))
938 }
939
940 fn rename(&self, _from: &Path, _to: &Path) -> std::io::Result<()> {
941 Err(std::io::Error::new(
942 std::io::ErrorKind::Unsupported,
943 "rename not supported in UiTestFs",
944 ))
945 }
946
947 fn remove_file(&self, _path: &Path) -> std::io::Result<()> {
948 Err(std::io::Error::new(
949 std::io::ErrorKind::Unsupported,
950 "remove_file not supported in UiTestFs",
951 ))
952 }
953
954 fn remove_dir(&self, _path: &Path) -> std::io::Result<()> {
955 Err(std::io::Error::new(
956 std::io::ErrorKind::Unsupported,
957 "remove_dir not supported in UiTestFs",
958 ))
959 }
960
961 fn remove_dir_all(&self, _path: &Path) -> std::io::Result<()> {
962 Err(std::io::Error::new(
963 std::io::ErrorKind::Unsupported,
964 "remove_dir_all not supported in UiTestFs",
965 ))
966 }
967
968 fn copy_file(&self, _from: &Path, _to: &Path) -> std::io::Result<u64> {
969 Err(std::io::Error::new(
970 std::io::ErrorKind::Unsupported,
971 "copy_file not supported in UiTestFs",
972 ))
973 }
974 }
975
976 fn file_entry(path: &str) -> FsEntry {
977 let path = PathBuf::from(path);
978 let name = path
979 .file_name()
980 .and_then(|name| name.to_str())
981 .unwrap_or(path.as_os_str().to_string_lossy().as_ref())
982 .to_string();
983 FsEntry {
984 name,
985 path,
986 is_dir: false,
987 is_symlink: false,
988 size: None,
989 modified: None,
990 }
991 }
992 #[test]
993 fn list_column_layout_all_columns_visible_without_preview() {
994 let cfg = columns_config(
995 true,
996 true,
997 [
998 FileListDataColumn::Name,
999 FileListDataColumn::Extension,
1000 FileListDataColumn::Size,
1001 FileListDataColumn::Modified,
1002 ],
1003 );
1004 assert_eq!(
1005 list_column_layout(false, &cfg),
1006 ListColumnLayout {
1007 data_columns: vec![
1008 FileListDataColumn::Name,
1009 FileListDataColumn::Extension,
1010 FileListDataColumn::Size,
1011 FileListDataColumn::Modified,
1012 ],
1013 name: TableColumnIndex::new(0),
1014 extension: Some(TableColumnIndex::new(1)),
1015 size: Some(TableColumnIndex::new(2)),
1016 modified: Some(TableColumnIndex::new(3)),
1017 }
1018 );
1019 }
1020
1021 #[test]
1022 fn list_column_layout_hides_extension_column() {
1023 let mut cfg = columns_config(
1024 true,
1025 true,
1026 [
1027 FileListDataColumn::Name,
1028 FileListDataColumn::Extension,
1029 FileListDataColumn::Size,
1030 FileListDataColumn::Modified,
1031 ],
1032 );
1033 cfg.show_extension = false;
1034
1035 assert_eq!(
1036 list_column_layout(false, &cfg),
1037 ListColumnLayout {
1038 data_columns: vec![
1039 FileListDataColumn::Name,
1040 FileListDataColumn::Size,
1041 FileListDataColumn::Modified,
1042 ],
1043 name: TableColumnIndex::new(0),
1044 extension: None,
1045 size: Some(TableColumnIndex::new(1)),
1046 modified: Some(TableColumnIndex::new(2)),
1047 }
1048 );
1049 }
1050
1051 #[test]
1052 fn list_column_layout_all_columns_visible_with_preview() {
1053 let cfg = columns_config(
1054 true,
1055 true,
1056 [
1057 FileListDataColumn::Name,
1058 FileListDataColumn::Extension,
1059 FileListDataColumn::Size,
1060 FileListDataColumn::Modified,
1061 ],
1062 );
1063 assert_eq!(
1064 list_column_layout(true, &cfg),
1065 ListColumnLayout {
1066 data_columns: vec![
1067 FileListDataColumn::Name,
1068 FileListDataColumn::Extension,
1069 FileListDataColumn::Size,
1070 FileListDataColumn::Modified,
1071 ],
1072 name: TableColumnIndex::new(1),
1073 extension: Some(TableColumnIndex::new(2)),
1074 size: Some(TableColumnIndex::new(3)),
1075 modified: Some(TableColumnIndex::new(4)),
1076 }
1077 );
1078 }
1079
1080 #[test]
1081 fn list_column_layout_hides_size_column() {
1082 let cfg = columns_config(
1083 false,
1084 true,
1085 [
1086 FileListDataColumn::Name,
1087 FileListDataColumn::Extension,
1088 FileListDataColumn::Size,
1089 FileListDataColumn::Modified,
1090 ],
1091 );
1092 assert_eq!(
1093 list_column_layout(false, &cfg),
1094 ListColumnLayout {
1095 data_columns: vec![
1096 FileListDataColumn::Name,
1097 FileListDataColumn::Extension,
1098 FileListDataColumn::Modified,
1099 ],
1100 name: TableColumnIndex::new(0),
1101 extension: Some(TableColumnIndex::new(1)),
1102 size: None,
1103 modified: Some(TableColumnIndex::new(2)),
1104 }
1105 );
1106 }
1107
1108 #[test]
1109 fn apply_file_list_view_from_ui_rejects_thumbnail_views_without_backend() {
1110 let mut state = FileDialogState::new(DialogMode::OpenFile);
1111 state.ui.config.file_list_view = FileListViewMode::List;
1112 state.ui.config.thumbnails_enabled = false;
1113 state.ui.config.file_list_columns.show_preview = false;
1114
1115 assert!(!apply_file_list_view_from_ui(
1116 &mut state,
1117 FileListViewMode::ThumbnailsList,
1118 false
1119 ));
1120 assert_eq!(state.ui.config.file_list_view, FileListViewMode::List);
1121 assert!(!state.ui.config.thumbnails_enabled);
1122 assert!(!state.ui.config.file_list_columns.show_preview);
1123
1124 assert!(!apply_file_list_view_from_ui(
1125 &mut state,
1126 FileListViewMode::Grid,
1127 false
1128 ));
1129 assert_eq!(state.ui.config.file_list_view, FileListViewMode::List);
1130 assert!(!state.ui.config.thumbnails_enabled);
1131 }
1132
1133 #[test]
1134 fn apply_file_list_view_from_ui_enables_thumbnail_state_with_backend() {
1135 let mut state = FileDialogState::new(DialogMode::OpenFile);
1136 state.ui.config.file_list_columns.show_preview = false;
1137
1138 assert!(apply_file_list_view_from_ui(
1139 &mut state,
1140 FileListViewMode::ThumbnailsList,
1141 true
1142 ));
1143 assert_eq!(
1144 state.ui.config.file_list_view,
1145 FileListViewMode::ThumbnailsList
1146 );
1147 assert!(state.ui.config.thumbnails_enabled);
1148 assert!(state.ui.config.file_list_columns.show_preview);
1149
1150 state.ui.config.file_list_columns.show_preview = false;
1151 assert!(apply_file_list_view_from_ui(
1152 &mut state,
1153 FileListViewMode::Grid,
1154 true
1155 ));
1156 assert_eq!(state.ui.config.file_list_view, FileListViewMode::Grid);
1157 assert!(state.ui.config.thumbnails_enabled);
1158 assert!(!state.ui.config.file_list_columns.show_preview);
1159 }
1160
1161 #[test]
1162 fn apply_file_list_view_from_ui_keeps_list_view_available() {
1163 let mut state = FileDialogState::new(DialogMode::OpenFile);
1164 state.ui.config.file_list_view = FileListViewMode::Grid;
1165 state.ui.config.thumbnails_enabled = true;
1166
1167 assert!(apply_file_list_view_from_ui(
1168 &mut state,
1169 FileListViewMode::List,
1170 false
1171 ));
1172 assert_eq!(state.ui.config.file_list_view, FileListViewMode::List);
1173 assert!(state.ui.config.thumbnails_enabled);
1174 }
1175
1176 #[test]
1177 fn list_column_layout_hides_modified_column() {
1178 let cfg = columns_config(
1179 true,
1180 false,
1181 [
1182 FileListDataColumn::Name,
1183 FileListDataColumn::Extension,
1184 FileListDataColumn::Size,
1185 FileListDataColumn::Modified,
1186 ],
1187 );
1188 assert_eq!(
1189 list_column_layout(false, &cfg),
1190 ListColumnLayout {
1191 data_columns: vec![
1192 FileListDataColumn::Name,
1193 FileListDataColumn::Extension,
1194 FileListDataColumn::Size,
1195 ],
1196 name: TableColumnIndex::new(0),
1197 extension: Some(TableColumnIndex::new(1)),
1198 size: Some(TableColumnIndex::new(2)),
1199 modified: None,
1200 }
1201 );
1202 }
1203
1204 #[test]
1205 fn list_column_layout_hides_size_and_modified_columns() {
1206 let cfg = columns_config(
1207 false,
1208 false,
1209 [
1210 FileListDataColumn::Name,
1211 FileListDataColumn::Extension,
1212 FileListDataColumn::Size,
1213 FileListDataColumn::Modified,
1214 ],
1215 );
1216 assert_eq!(
1217 list_column_layout(false, &cfg),
1218 ListColumnLayout {
1219 data_columns: vec![FileListDataColumn::Name, FileListDataColumn::Extension],
1220 name: TableColumnIndex::new(0),
1221 extension: Some(TableColumnIndex::new(1)),
1222 size: None,
1223 modified: None,
1224 }
1225 );
1226 }
1227
1228 #[test]
1229 fn list_column_layout_respects_custom_order() {
1230 let cfg = columns_config(
1231 true,
1232 true,
1233 [
1234 FileListDataColumn::Name,
1235 FileListDataColumn::Size,
1236 FileListDataColumn::Modified,
1237 FileListDataColumn::Extension,
1238 ],
1239 );
1240 assert_eq!(
1241 list_column_layout(false, &cfg),
1242 ListColumnLayout {
1243 data_columns: vec![
1244 FileListDataColumn::Name,
1245 FileListDataColumn::Size,
1246 FileListDataColumn::Modified,
1247 FileListDataColumn::Extension,
1248 ],
1249 name: TableColumnIndex::new(0),
1250 extension: Some(TableColumnIndex::new(3)),
1251 size: Some(TableColumnIndex::new(1)),
1252 modified: Some(TableColumnIndex::new(2)),
1253 }
1254 );
1255 }
1256
1257 #[test]
1258 fn merged_order_with_current_keeps_hidden_columns() {
1259 let merged = merged_order_with_current(
1260 &[FileListDataColumn::Name, FileListDataColumn::Modified],
1261 [
1262 FileListDataColumn::Name,
1263 FileListDataColumn::Size,
1264 FileListDataColumn::Modified,
1265 FileListDataColumn::Extension,
1266 ],
1267 );
1268 assert_eq!(
1269 merged,
1270 [
1271 FileListDataColumn::Name,
1272 FileListDataColumn::Modified,
1273 FileListDataColumn::Size,
1274 FileListDataColumn::Extension,
1275 ]
1276 );
1277 }
1278
1279 #[test]
1280 fn move_column_order_up_swaps_adjacent_items() {
1281 let mut order = [
1282 FileListDataColumn::Name,
1283 FileListDataColumn::Extension,
1284 FileListDataColumn::Size,
1285 FileListDataColumn::Modified,
1286 ];
1287 assert!(super::file_table::move_column_order_up(&mut order, 2));
1288 assert_eq!(
1289 order,
1290 [
1291 FileListDataColumn::Name,
1292 FileListDataColumn::Size,
1293 FileListDataColumn::Extension,
1294 FileListDataColumn::Modified,
1295 ]
1296 );
1297 }
1298
1299 #[test]
1300 fn move_column_order_down_swaps_adjacent_items() {
1301 let mut order = [
1302 FileListDataColumn::Name,
1303 FileListDataColumn::Extension,
1304 FileListDataColumn::Size,
1305 FileListDataColumn::Modified,
1306 ];
1307 assert!(super::file_table::move_column_order_down(&mut order, 1));
1308 assert_eq!(
1309 order,
1310 [
1311 FileListDataColumn::Name,
1312 FileListDataColumn::Size,
1313 FileListDataColumn::Extension,
1314 FileListDataColumn::Modified,
1315 ]
1316 );
1317 }
1318
1319 #[test]
1320 fn move_column_order_up_rejects_first_item() {
1321 let mut order = [
1322 FileListDataColumn::Name,
1323 FileListDataColumn::Extension,
1324 FileListDataColumn::Size,
1325 FileListDataColumn::Modified,
1326 ];
1327 assert!(!super::file_table::move_column_order_up(&mut order, 0));
1328 assert_eq!(
1329 order,
1330 [
1331 FileListDataColumn::Name,
1332 FileListDataColumn::Extension,
1333 FileListDataColumn::Size,
1334 FileListDataColumn::Modified,
1335 ]
1336 );
1337 }
1338
1339 #[test]
1340 fn apply_compact_column_layout_updates_visibility_and_order_only() {
1341 let expected_weights = FileListColumnWeightOverrides {
1342 preview: Some(0.11),
1343 name: Some(0.57),
1344 extension: Some(0.14),
1345 size: Some(0.18),
1346 modified: Some(0.22),
1347 };
1348
1349 let mut cfg = FileListColumnsConfig {
1350 show_preview: true,
1351 show_extension: true,
1352 show_size: false,
1353 show_modified: true,
1354 order: [
1355 FileListDataColumn::Modified,
1356 FileListDataColumn::Size,
1357 FileListDataColumn::Extension,
1358 FileListDataColumn::Name,
1359 ],
1360 weight_overrides: expected_weights.clone(),
1361 };
1362
1363 super::file_table::apply_compact_column_layout(&mut cfg);
1364
1365 assert!(!cfg.show_preview);
1366 assert!(cfg.show_size);
1367 assert!(!cfg.show_modified);
1368 assert_eq!(
1369 cfg.order,
1370 [
1371 FileListDataColumn::Name,
1372 FileListDataColumn::Extension,
1373 FileListDataColumn::Size,
1374 FileListDataColumn::Modified,
1375 ]
1376 );
1377 assert_eq!(cfg.weight_overrides, expected_weights);
1378 }
1379
1380 #[test]
1381 fn apply_balanced_column_layout_updates_visibility_and_order_only() {
1382 let expected_weights = FileListColumnWeightOverrides {
1383 preview: Some(0.13),
1384 name: Some(0.54),
1385 extension: Some(0.16),
1386 size: Some(0.17),
1387 modified: Some(0.21),
1388 };
1389
1390 let mut cfg = FileListColumnsConfig {
1391 show_preview: false,
1392 show_extension: true,
1393 show_size: false,
1394 show_modified: false,
1395 order: [
1396 FileListDataColumn::Size,
1397 FileListDataColumn::Name,
1398 FileListDataColumn::Modified,
1399 FileListDataColumn::Extension,
1400 ],
1401 weight_overrides: expected_weights.clone(),
1402 };
1403
1404 super::file_table::apply_balanced_column_layout(&mut cfg);
1405
1406 assert!(cfg.show_preview);
1407 assert!(cfg.show_size);
1408 assert!(cfg.show_modified);
1409 assert_eq!(
1410 cfg.order,
1411 [
1412 FileListDataColumn::Name,
1413 FileListDataColumn::Extension,
1414 FileListDataColumn::Size,
1415 FileListDataColumn::Modified,
1416 ]
1417 );
1418 assert_eq!(cfg.weight_overrides, expected_weights);
1419 }
1420
1421 #[test]
1422 fn open_rename_modal_from_selection_prefills_name_from_id() {
1423 let mut state = FileDialogState::new(DialogMode::OpenFiles);
1424 state.core.set_cwd(PathBuf::from("/tmp"));
1425
1426 let fs = UiTestFs {
1427 entries: vec![file_entry("/tmp/a.txt")],
1428 };
1429 state.core.rescan_if_needed(&fs);
1430
1431 let id = state
1432 .core
1433 .entries()
1434 .iter()
1435 .find(|entry| entry.path == Path::new("/tmp/a.txt"))
1436 .map(|entry| entry.id)
1437 .expect("missing /tmp/a.txt entry id");
1438 state.core.focus_and_select_by_id(id);
1439
1440 open_rename_modal_from_selection(&mut state);
1441
1442 assert_eq!(state.ui.operations.rename.target_id, Some(id));
1443 assert_eq!(state.ui.operations.rename.to, "a.txt");
1444 assert!(state.ui.operations.rename.open_next);
1445 assert!(state.ui.operations.rename.focus_next);
1446 }
1447
1448 #[test]
1449 fn open_rename_modal_from_selection_ignores_unresolved_id() {
1450 let mut state = FileDialogState::new(DialogMode::OpenFiles);
1451 let id = EntryId::from_path(Path::new("/tmp/missing.txt"));
1452 state.core.focus_and_select_by_id(id);
1453
1454 open_rename_modal_from_selection(&mut state);
1455
1456 assert_eq!(state.ui.operations.rename.target_id, None);
1457 assert!(state.ui.operations.rename.to.is_empty());
1458 assert!(!state.ui.operations.rename.open_next);
1459 }
1460
1461 #[test]
1462 fn open_delete_modal_from_selection_stores_selected_ids() {
1463 let mut state = FileDialogState::new(DialogMode::OpenFiles);
1464 state.core.set_cwd(PathBuf::from("/tmp"));
1465
1466 let fs = UiTestFs {
1467 entries: vec![file_entry("/tmp/a.txt"), file_entry("/tmp/b.txt")],
1468 };
1469 state.core.rescan_if_needed(&fs);
1470
1471 let a = state
1472 .core
1473 .entries()
1474 .iter()
1475 .find(|entry| entry.path == Path::new("/tmp/a.txt"))
1476 .map(|entry| entry.id)
1477 .expect("missing /tmp/a.txt entry id");
1478 let b = state
1479 .core
1480 .entries()
1481 .iter()
1482 .find(|entry| entry.path == Path::new("/tmp/b.txt"))
1483 .map(|entry| entry.id)
1484 .expect("missing /tmp/b.txt entry id");
1485 state.core.replace_selection_by_ids([b, a]);
1486
1487 open_delete_modal_from_selection(&mut state);
1488
1489 assert_eq!(state.ui.operations.delete.target_ids, vec![b, a]);
1490 assert!(state.ui.operations.delete.open_next);
1491 }
1492
1493 #[test]
1494 fn operation_state_defaults_keep_modal_jobs_internal() {
1495 let state = FileDialogState::new(DialogMode::OpenFiles);
1496
1497 assert!(state.ui.operations.rename.target_id.is_none());
1498 assert!(!state.ui.operations.rename.open_next);
1499 assert!(state.ui.operations.rename.to.is_empty());
1500 assert!(state.ui.operations.delete.target_ids.is_empty());
1501 assert!(!state.ui.operations.delete.open_next);
1502 assert!(state.ui.operations.paste.clipboard.is_none());
1503 assert!(state.ui.operations.paste.job.is_none());
1504 assert!(!state.ui.operations.paste.conflict_open_next);
1505 assert!(state.ui.operations.places.io.buffer.is_empty());
1506 assert!(state.ui.operations.places.edit.group.is_empty());
1507 assert!(state.ui.operations.places.inline_edit.target.is_none());
1508 }
1509}