| 20 |
20 |
|
//! # What this module is, and what it deliberately is not
|
| 21 |
21 |
|
//!
|
| 22 |
22 |
|
//! It is the *host* half, and it is small on purpose: plumbing against described
|
| 23 |
|
- |
//! screens that know nothing about egui. Everything it does is one of four
|
|
23 |
+ |
//! screens that know nothing about egui. Everything it does is one of five
|
| 24 |
24 |
|
//! things, and none of them is drawing:
|
| 25 |
25 |
|
//!
|
| 26 |
26 |
|
//! 1. resolve the host facts the screens need (themes, the palette) and adapt
|
| 27 |
27 |
|
//! the app's own handles to the narrow traits the screens borrow,
|
| 28 |
28 |
|
//! 2. hold each [`Runtime`] across frames, because a frame does not outlive itself,
|
| 29 |
29 |
|
//! 3. hand a [`Step`] to the router and the answer back to the runtime,
|
| 30 |
|
- |
//! 4. put a route failure somewhere the user can see it.
|
|
30 |
+ |
//! 4. put a route failure somewhere the user can see it,
|
|
31 |
+ |
//! 5. perform what a description asked the host for and cannot say itself: a
|
|
32 |
+ |
//! file picker ([`locate`]), a place to save ([`hand_over`]), the way back
|
|
33 |
+ |
//! ([`leave`]), and the one gesture that is not an outcome at all
|
|
34 |
+ |
//! ([`dragging_out`], the drag into a DAW).
|
| 31 |
35 |
|
//!
|
| 32 |
36 |
|
//! There is no `if let Node::...` anywhere here, and there should never be one.
|
| 33 |
37 |
|
//! The moment this file starts deciding what a node looks like, the drawing has
|
| 187 |
191 |
|
inline(ui, &mut runtime, &host, "/", true);
|
| 188 |
192 |
|
state.described.shell = runtime;
|
| 189 |
193 |
|
apply(ui.ctx(), state, sync, intents.into_inner());
|
|
194 |
+ |
|
|
195 |
+ |
// The one gesture the description does not carry, after the intents rather
|
|
196 |
+ |
// than before them: what a press chose is what a drag should carry out.
|
|
197 |
+ |
if dragging_out(ui, state) {
|
|
198 |
+ |
crate::ui::file_list_menus::start_os_drag(state);
|
|
199 |
+ |
}
|
| 190 |
200 |
|
}
|
| 191 |
201 |
|
|
| 192 |
202 |
|
/// Draw the described sample editor, and act on whatever was pressed.
|
| 2408 |
2418 |
|
});
|
| 2409 |
2419 |
|
}
|
| 2410 |
2420 |
|
|
|
2421 |
+ |
/// How far the pointer travels before a press over the list counts as a drag.
|
|
2422 |
+ |
///
|
|
2423 |
+ |
/// The shipped list's number, kept: four points is far enough that a press with
|
|
2424 |
+ |
/// a shaky hand still opens the row it landed on.
|
|
2425 |
+ |
const DRAG_THRESHOLD: f32 = 4.0;
|
|
2426 |
+ |
|
|
2427 |
+ |
/// How long the drag cooldown holds when egui never sees the button released.
|
|
2428 |
+ |
///
|
|
2429 |
+ |
/// The safety net rather than the mechanism: an OS drag that ends over another
|
|
2430 |
+ |
/// application swallows the mouse-up, and the pointer reads as held forever. Two
|
|
2431 |
+ |
/// seconds is the shipped list's number.
|
|
2432 |
+ |
const DRAG_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(2);
|
|
2433 |
+ |
|
|
2434 |
+ |
/// Drag what is chosen out of the file list, which is the host's half of it.
|
|
2435 |
+ |
///
|
|
2436 |
+ |
/// The host half of a *gesture*, where [`locate`] and [`hand_over`] are host
|
|
2437 |
+ |
/// halves of an outcome, and it is here for the reason `quasi::files` records in
|
|
2438 |
+ |
/// its header: an OS drag that ends outside the window leaves egui's pointer
|
|
2439 |
+ |
/// state stale, and the cooldown that fixes that is a fact about this host's
|
|
2440 |
+ |
/// input rather than about a sample. So the renderer reaches for
|
|
2441 |
+ |
/// [`start_os_drag`](crate::ui::file_list_menus::start_os_drag) directly and the
|
|
2442 |
+ |
/// description says nothing.
|
|
2443 |
+ |
///
|
|
2444 |
+ |
/// It went missing in `49b7429`, which replaced the shipped list with the
|
|
2445 |
+ |
/// described one and took the five modules that drew it with it. Dragging a
|
|
2446 |
+ |
/// sample into a DAW is close to the point of a sample manager, so a flip that
|
|
2447 |
+ |
/// loses it has failed the same way the tag-undo finding said a flip that loses
|
|
2448 |
+ |
/// Cmd+Z has. Restored 2026-08-25, ruled by Max on audiofiles `edd4d1d8`.
|
|
2449 |
+ |
///
|
|
2450 |
+ |
/// # What the gesture can be told apart from, and what it cannot
|
|
2451 |
+ |
///
|
|
2452 |
+ |
/// A described table hands the host no rows. `quasi-immediate` senses each cell
|
|
2453 |
+ |
/// for a click and a menu, and neither the cell's rect nor its index reaches
|
|
2454 |
+ |
/// this side, so what is read here is egui's raw pointer plus the room the shell
|
|
2455 |
+ |
/// was given. Three guards do the telling apart:
|
|
2456 |
+ |
///
|
|
2457 |
+ |
/// - **A drag-sense widget owning the gesture.** A scrollbar, a panel splitter,
|
|
2458 |
+ |
/// a slider and a text selection are all `Sense::drag`, so `dragged_id` is set
|
|
2459 |
+ |
/// and this stands aside. That is the whole of the dangerous set: everything
|
|
2460 |
+ |
/// else under the pointer is a click widget.
|
|
2461 |
+ |
/// - **The instrument panel.** A sample dragged while it is open is being
|
|
2462 |
+ |
/// assigned to a zone, which is egui's own drag-and-drop and not the OS's.
|
|
2463 |
+ |
/// The shipped list made the same call.
|
|
2464 |
+ |
/// - **The room.** The press has to have landed inside the pane the shell was
|
|
2465 |
+ |
/// drawn into, which leaves out the filter panel, the detail panel and every
|
|
2466 |
+ |
/// floating window.
|
|
2467 |
+ |
///
|
|
2468 |
+ |
/// **What it cannot tell apart is a press on the toolbar from a press on a
|
|
2469 |
+ |
/// row**, and what follows from that is the one behaviour that is not the
|
|
2470 |
+ |
/// shipped list's: this drags *what is chosen* rather than the row under the
|
|
2471 |
+ |
/// cursor, where the shipped list collapsed the selection onto that row first.
|
|
2472 |
+ |
/// Pressing a chosen row and dragging is the ordinary case and is exact;
|
|
2473 |
+ |
/// pressing an unchosen one drags the chosen set instead, which the drag image
|
|
2474 |
+ |
/// and the status line both name. The per-row hover that said "Just dragged.
|
|
2475 |
+ |
/// Ready again in a moment." is gone with the rows for the same reason. Closing
|
|
2476 |
+ |
/// either wants the description to say where its rows are, which is a question
|
|
2477 |
+ |
/// for the vocabulary rather than something to guess at here.
|
|
2478 |
+ |
fn dragging_out(ui: &egui::Ui, state: &mut BrowserState) -> bool {
|
|
2479 |
+ |
let ctx = ui.ctx();
|
|
2480 |
+ |
let (held, origin, latest) = ctx.input(|input| {
|
|
2481 |
+ |
(
|
|
2482 |
+ |
input.pointer.button_down(egui::PointerButton::Primary),
|
|
2483 |
+ |
input.pointer.press_origin(),
|
|
2484 |
+ |
input.pointer.latest_pos(),
|
|
2485 |
+ |
)
|
|
2486 |
+ |
});
|
|
2487 |
+ |
|
|
2488 |
+ |
if cooling_down(state, held) {
|
|
2489 |
+ |
return false;
|
|
2490 |
+ |
}
|
|
2491 |
+ |
if !held || state.preview.instrument_visible {
|
|
2492 |
+ |
return false;
|
|
2493 |
+ |
}
|
|
2494 |
+ |
// Something that really is a drag has the gesture: a scrollbar, a splitter,
|
|
2495 |
+ |
// a slider, a caret sweeping a text field.
|
|
2496 |
+ |
if ctx.dragged_id().is_some() {
|
|
2497 |
+ |
return false;
|
|
2498 |
+ |
}
|
|
2499 |
+ |
if state.nav.selection.count() == 0 {
|
|
2500 |
+ |
return false;
|
|
2501 |
+ |
}
|
|
2502 |
+ |
let (Some(origin), Some(latest)) = (origin, latest) else {
|
|
2503 |
+ |
return false;
|
|
2504 |
+ |
};
|
|
2505 |
+ |
(latest - origin).length() > DRAG_THRESHOLD && ui.max_rect().contains(origin)
|
|
2506 |
+ |
}
|
|
2507 |
+ |
|
|
2508 |
+ |
/// Whether the last OS drag is still holding this one off.
|
|
2509 |
+ |
///
|
|
2510 |
+ |
/// After a drag that ends over another application, egui never sees the button
|
|
2511 |
+ |
/// come up: the pointer reads as held down and the next frame would start a
|
|
2512 |
+ |
/// second drag from the same press. So a drag sets [`BrowserState::os_drag_cooldown`]
|
|
2513 |
+ |
/// and this clears it on the first frame where the pointer is genuinely up, or
|
|
2514 |
+ |
/// when [`DRAG_COOLDOWN`] says the button-up is never coming.
|
|
2515 |
+ |
///
|
|
2516 |
+ |
/// Separate from [`dragging_out`] because it is the whole of the stale-pointer
|
|
2517 |
+ |
/// problem and is the one part of the gesture that can be tested without a
|
|
2518 |
+ |
/// pointer.
|
|
2519 |
+ |
fn cooling_down(state: &mut BrowserState, held: bool) -> bool {
|
|
2520 |
+ |
let Some(since) = state.os_drag_cooldown else {
|
|
2521 |
+ |
return false;
|
|
2522 |
+ |
};
|
|
2523 |
+ |
if !held || since.elapsed() > DRAG_COOLDOWN {
|
|
2524 |
+ |
state.os_drag_cooldown = None;
|
|
2525 |
+ |
return false;
|
|
2526 |
+ |
}
|
|
2527 |
+ |
true
|
|
2528 |
+ |
}
|
|
2529 |
+ |
|
| 2411 |
2530 |
|
/// Ask the host where something goes, and write the answer down for later.
|
| 2412 |
2531 |
|
///
|
| 2413 |
2532 |
|
/// The host half of `Outcome::Locate`, which quasi ruled and shipped in 0.60
|
| 2746 |
2865 |
|
state.detail.selected_tags
|
| 2747 |
2866 |
|
);
|
| 2748 |
2867 |
|
}
|
|
2868 |
+ |
|
|
2869 |
+ |
/// The stale-pointer half of the drag out, which is the half that can be
|
|
2870 |
+ |
/// tested without a pointer. See [`cooling_down`].
|
|
2871 |
+ |
#[test]
|
|
2872 |
+ |
fn the_drag_cooldown_ends_when_the_pointer_comes_up() {
|
|
2873 |
+ |
let (mut state, _dir) = chosen();
|
|
2874 |
+ |
state.os_drag_cooldown = Some(std::time::Instant::now());
|
|
2875 |
+ |
|
|
2876 |
+ |
assert!(
|
|
2877 |
+ |
cooling_down(&mut state, true),
|
|
2878 |
+ |
"still held, so the next frame must not start a second drag"
|
|
2879 |
+ |
);
|
|
2880 |
+ |
assert!(
|
|
2881 |
+ |
!cooling_down(&mut state, false),
|
|
2882 |
+ |
"let go, so the gesture is available again"
|
|
2883 |
+ |
);
|
|
2884 |
+ |
assert!(
|
|
2885 |
+ |
state.os_drag_cooldown.is_none(),
|
|
2886 |
+ |
"and nothing is remembered"
|
|
2887 |
+ |
);
|
|
2888 |
+ |
}
|
|
2889 |
+ |
|
|
2890 |
+ |
/// The case the timeout exists for: a drag that ended over another
|
|
2891 |
+ |
/// application, whose mouse-up egui never saw.
|
|
2892 |
+ |
#[test]
|
|
2893 |
+ |
fn the_drag_cooldown_expires_when_no_button_up_arrives() {
|
|
2894 |
+ |
let (mut state, _dir) = chosen();
|
|
2895 |
+ |
state.os_drag_cooldown = std::time::Instant::now().checked_sub(DRAG_COOLDOWN * 2);
|
|
2896 |
+ |
assert!(state.os_drag_cooldown.is_some(), "the clock went back");
|
|
2897 |
+ |
|
|
2898 |
+ |
assert!(
|
|
2899 |
+ |
!cooling_down(&mut state, true),
|
|
2900 |
+ |
"the pointer still reads as held and the wait is over anyway"
|
|
2901 |
+ |
);
|
|
2902 |
+ |
assert!(state.os_drag_cooldown.is_none());
|
|
2903 |
+ |
}
|
| 2749 |
2904 |
|
}
|