Skip to content

Commit ba6ad23

Browse files
committed
Improve native panel interactions and texture history
1 parent eef3fa8 commit ba6ad23

22 files changed

Lines changed: 911 additions & 224 deletions

File tree

‎native/src/sbc/panels/controls/grid.rs‎

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,39 @@ pub(crate) fn list_asset_tree(
384384
items
385385
}
386386

387+
pub(crate) fn list_assets(
388+
interface: &NativeInterfaceRef,
389+
dir: &str,
390+
extensions: &[&str],
391+
) -> Vec<GridItem> {
392+
list_entries(interface, dir, extensions, false)
393+
}
394+
395+
/// Like [`list_assets`], but also lists sub-directories as browsable folder
396+
/// cells. Only the Open/Load dialog wants that: a project is an `.sdd` folder,
397+
/// invisible to the engine's file-only `ListDir`. Texture pickers stay
398+
/// file-only, or engine dirs (`bitmaps/`) would bury the textures under dozens
399+
/// of unrelated sub-folders.
400+
pub(crate) fn list_entries_with_dirs(
401+
interface: &NativeInterfaceRef,
402+
dir: &str,
403+
extensions: &[&str],
404+
) -> Vec<GridItem> {
405+
list_entries(interface, dir, extensions, true)
406+
}
407+
408+
/// The parent of a VFS directory, or None at the root.
409+
pub(crate) fn parent_dir(dir: &str) -> Option<String> {
410+
let trimmed = dir.trim_end_matches('/');
411+
if trimmed.is_empty() {
412+
return None;
413+
}
414+
match trimmed.rsplit_once('/') {
415+
Some((parent, _)) => Some(parent.to_string()),
416+
None => Some(String::new()),
417+
}
418+
}
419+
387420
/// The inline brush grid follows the same location/root split as
388421
/// [`list_asset_tree`], but brush commands consume direct VFS paths. Directory
389422
/// ids stay asset-relative so navigation stays within the asset-pack tree;
@@ -426,39 +459,6 @@ fn asset_pack_directory(root_dir: &str, location: &str) -> String {
426459
directory
427460
}
428461

429-
pub(crate) fn list_assets(
430-
interface: &NativeInterfaceRef,
431-
dir: &str,
432-
extensions: &[&str],
433-
) -> Vec<GridItem> {
434-
list_entries(interface, dir, extensions, false)
435-
}
436-
437-
/// Like [`list_assets`], but also lists sub-directories as browsable folder
438-
/// cells. Only the Open/Load dialog wants that: a project is an `.sdd` folder,
439-
/// invisible to the engine's file-only `ListDir`. Texture pickers stay
440-
/// file-only, or engine dirs (`bitmaps/`) would bury the textures under dozens
441-
/// of unrelated sub-folders.
442-
pub(crate) fn list_entries_with_dirs(
443-
interface: &NativeInterfaceRef,
444-
dir: &str,
445-
extensions: &[&str],
446-
) -> Vec<GridItem> {
447-
list_entries(interface, dir, extensions, true)
448-
}
449-
450-
/// The parent of a VFS directory, or None at the root.
451-
pub(crate) fn parent_dir(dir: &str) -> Option<String> {
452-
let trimmed = dir.trim_end_matches('/');
453-
if trimmed.is_empty() {
454-
return None;
455-
}
456-
match trimmed.rsplit_once('/') {
457-
Some((parent, _)) => Some(parent.to_string()),
458-
None => Some(String::new()),
459-
}
460-
}
461-
462462
fn list_entries(
463463
interface: &NativeInterfaceRef,
464464
dir: &str,

‎native/src/sbc/panels/cursor/drag_cursor.rs‎

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,8 @@ pub(crate) struct DragCursor {
2323
}
2424

2525
impl DragCursor {
26-
/// Where the pointer is pinned. The drag's delta is measured from here,
27-
/// because the pointer is put back on it after every move.
28-
pub(crate) fn anchor_x(&self) -> Option<f32> {
29-
self.anchor.map(|(x, _)| x as f32)
30-
}
31-
32-
pub(crate) fn begin(&mut self, interface: &NativeInterfaceRef) {
33-
let Ok(mouse) = interface.input().get_mouse_state() else {
34-
return;
35-
};
36-
self.anchor = Some((mouse.x as i32, mouse.y as i32));
26+
pub(crate) fn begin(&mut self, interface: &NativeInterfaceRef, anchor: (i32, i32)) {
27+
self.anchor = Some(anchor);
3728

3829
let ctrl = interface.unsynced_ctrl();
3930
if !self.assigned {
@@ -45,18 +36,15 @@ impl DragCursor {
4536
let _ = ctrl.set_mouse_cursor(EMPTY_CURSOR, 1.0);
4637
}
4738

48-
/// Pin the pointer back to the anchor. Called after the value has taken the
49-
/// movement, so the motion is consumed rather than lost.
50-
pub(crate) fn hold(&self, interface: &NativeInterfaceRef) {
51-
let Some((x, y)) = self.anchor else {
52-
return;
53-
};
54-
let ctrl = interface.unsynced_ctrl();
55-
let _ = ctrl.warp_mouse(x, y);
39+
/// Re-assert the empty cursor while a drag is active. Motion itself is
40+
/// pinned synchronously from RmlUi's `drag` listener.
41+
pub(crate) fn reassert(&self, interface: &NativeInterfaceRef) {
5642
// Re-assert it every tick: the engine syncs the cursor to whatever RmlUi
5743
// is hovering on each update, so setting it once at dragstart is undone
5844
// on the very next frame.
59-
let _ = ctrl.set_mouse_cursor(EMPTY_CURSOR, 1.0);
45+
let _ = interface
46+
.unsynced_ctrl()
47+
.set_mouse_cursor(EMPTY_CURSOR, 1.0);
6048
}
6149

6250
pub(crate) fn end(&mut self, interface: &NativeInterfaceRef) {
@@ -68,4 +56,13 @@ impl DragCursor {
6856
// `SB.SetMouseCursor()` does.
6957
let _ = ctrl.set_mouse_cursor("", 1.0);
7058
}
59+
60+
/// End an interrupted drag without moving the cursor back into the panel.
61+
/// A new press is an explicit user decision about where the pointer is, so
62+
/// preserving that location avoids turning a right-click on the map into a
63+
/// surprise cursor teleport.
64+
pub(crate) fn cancel(&mut self, interface: &NativeInterfaceRef) {
65+
self.anchor = None;
66+
let _ = interface.unsynced_ctrl().set_mouse_cursor("", 1.0);
67+
}
7168
}

‎native/src/sbc/panels/field.rs‎

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ pub(crate) fn new_interaction_queue() -> InteractionQueue {
3434
pub enum InteractionEvent {
3535
PointerDown {
3636
field: String,
37+
/// Engine mouse coordinates, when the RmlUi event provided them.
38+
/// Generic pointer users do not need an anchor; numeric drags do.
39+
anchor: Option<(i32, i32)>,
3740
},
3841
PointerUp {
3942
field: String,
@@ -46,6 +49,26 @@ pub enum InteractionEvent {
4649
DragEnd {
4750
field: String,
4851
},
52+
/// Exact horizontal movement from one RmlUi drag event. RmlUi emits this
53+
/// before the pointer is put back at its drag anchor.
54+
DragMove {
55+
field: String,
56+
dx: f32,
57+
},
58+
/// A numeric field asks the panel-owned presentation surface to follow its
59+
/// drag. The field describes values; the surface owns all RmlUi geometry.
60+
NumericDragPresentation(NumericDragPresentation),
61+
}
62+
63+
#[derive(Debug, Clone)]
64+
pub struct NumericDragPresentation {
65+
pub element: u64,
66+
pub title: String,
67+
pub value: String,
68+
pub min: Option<String>,
69+
pub max: Option<String>,
70+
/// A bounded range normalised to 0..1. `None` means no fill.
71+
pub progress: Option<f32>,
4972
}
5073

5174
/// One field as the control channel sees it: what it is called, what it holds,
@@ -183,7 +206,10 @@ pub(crate) fn on_pointer(
183206
for (event, make) in [
184207
(
185208
"mousedown",
186-
(|field| InteractionEvent::PointerDown { field }) as fn(String) -> InteractionEvent,
209+
(|field| InteractionEvent::PointerDown {
210+
field,
211+
anchor: None,
212+
}) as fn(String) -> InteractionEvent,
187213
),
188214
("mouseup", |field| InteractionEvent::PointerUp { field }),
189215
("dragstart", |field| InteractionEvent::DragStart { field }),
@@ -200,6 +226,127 @@ pub(crate) fn on_pointer(
200226
Ok(())
201227
}
202228

229+
/// Register numeric drag events with RmlUi's per-motion coordinates.
230+
///
231+
/// RmlUi captures a numeric field's drag before SBC receives normal mouse
232+
/// callbacks. Polling the engine cursor later made a small physical move look
233+
/// large when the original click landed away from the field's logical value.
234+
/// Instead, this mirrors Chili: consume the current RmlUi `drag` movement and
235+
/// immediately put the pointer back where the press began.
236+
pub(crate) fn on_numeric_pointer(
237+
interface: &NativeInterfaceRef,
238+
context: u64,
239+
element: u64,
240+
name: String,
241+
interactions: &InteractionQueue,
242+
) -> Result<(), Error> {
243+
let anchor = Rc::new(RefCell::new(None::<(i32, i32)>));
244+
245+
{
246+
let queue = interactions.clone();
247+
let field = name.clone();
248+
let anchor = anchor.clone();
249+
let iface = *interface;
250+
interface
251+
.rml_ui()
252+
.element_add_event_listener(element, "mousedown", false, move || {
253+
if current_rml_mouse_button(&iface) != Some(0) {
254+
return;
255+
}
256+
let Some((x, y)) = current_rml_mouse_position(&iface) else {
257+
return;
258+
};
259+
let Some(anchor_position) = engine_mouse_position(&iface, x, y) else {
260+
return;
261+
};
262+
*anchor.borrow_mut() = Some(anchor_position);
263+
let _ = iface
264+
.rml_ui()
265+
.context_set_pointer_capture(context, x, y, true);
266+
queue.borrow_mut().push(InteractionEvent::PointerDown {
267+
field: field.clone(),
268+
anchor: Some(anchor_position),
269+
});
270+
})?;
271+
}
272+
{
273+
let queue = interactions.clone();
274+
let field = name.clone();
275+
let iface = *interface;
276+
interface
277+
.rml_ui()
278+
.element_add_event_listener(element, "mouseup", false, move || {
279+
if current_rml_mouse_button(&iface) != Some(0) {
280+
return;
281+
}
282+
let _ = iface
283+
.rml_ui()
284+
.context_set_pointer_capture(context, 0, 0, false);
285+
queue.borrow_mut().push(InteractionEvent::PointerUp {
286+
field: field.clone(),
287+
});
288+
})?;
289+
}
290+
{
291+
let queue = interactions.clone();
292+
let field = name.clone();
293+
interface
294+
.rml_ui()
295+
.element_add_event_listener(element, "dragstart", false, move || {
296+
// RmlUi dispatches `dragstart` immediately before the first
297+
// `drag`. Do not warp here: it changes the context position
298+
// before that first `drag` can read its movement. The next
299+
// listener consumes and pins this same physical motion.
300+
queue.borrow_mut().push(InteractionEvent::DragStart {
301+
field: field.clone(),
302+
});
303+
})?;
304+
}
305+
{
306+
let queue = interactions.clone();
307+
let field = name.clone();
308+
let anchor = anchor.clone();
309+
let iface = *interface;
310+
interface
311+
.rml_ui()
312+
.element_add_event_listener(element, "drag", false, move || {
313+
let Some((mouse_x, _)) = current_rml_mouse_position(&iface) else {
314+
return;
315+
};
316+
let Some((anchor_x, anchor_y)) = *anchor.borrow() else {
317+
return;
318+
};
319+
let dx = (mouse_x - anchor_x) as f32;
320+
if dx != 0.0 {
321+
queue.borrow_mut().push(InteractionEvent::DragMove {
322+
field: field.clone(),
323+
dx,
324+
});
325+
// The backend's synthetic anchor move fires `drag` once
326+
// more with zero movement. It has already restored both
327+
// pointer positions, so avoid a redundant OS cursor warp.
328+
let _ = iface.unsynced_ctrl().warp_mouse(anchor_x, anchor_y);
329+
}
330+
})?;
331+
}
332+
{
333+
let queue = interactions.clone();
334+
let field = name;
335+
let iface = *interface;
336+
interface
337+
.rml_ui()
338+
.element_add_event_listener(element, "dragend", false, move || {
339+
let _ = iface
340+
.rml_ui()
341+
.context_set_pointer_capture(context, 0, 0, false);
342+
queue.borrow_mut().push(InteractionEvent::DragEnd {
343+
field: field.clone(),
344+
});
345+
})?;
346+
}
347+
Ok(())
348+
}
349+
203350
// ── DOM helpers ────────────────────────────────────────────────────
204351

205352
pub(crate) use crate::sbc::rml::{element_by_id, escape_rml};
@@ -298,3 +445,32 @@ pub trait Field {
298445
/// End edit mode — switch back to display, without committing.
299446
fn end_edit(&mut self, _interface: &NativeInterfaceRef) {}
300447
}
448+
449+
/// RmlUi event positions use top-origin screen coordinates; engine mouse and
450+
/// `warp_mouse` use the bottom-origin coordinates exposed by Spring's native
451+
/// input API.
452+
fn current_rml_mouse_position(interface: &NativeInterfaceRef) -> Option<(i32, i32)> {
453+
let rml = interface.rml_ui();
454+
let (event, ..) = rml.event_get_current().ok()?;
455+
let (x, has_x) = rml.event_get_parameter_int(event, "mouse_x").ok()?;
456+
let (y, has_y) = rml.event_get_parameter_int(event, "mouse_y").ok()?;
457+
if !has_x || !has_y {
458+
return None;
459+
}
460+
Some((x, y))
461+
}
462+
463+
/// RmlUi mouse buttons are zero-based: left, right, then middle.
464+
fn current_rml_mouse_button(interface: &NativeInterfaceRef) -> Option<i32> {
465+
let rml = interface.rml_ui();
466+
let (event, ..) = rml.event_get_current().ok()?;
467+
let (button, found) = rml.event_get_parameter_int(event, "button").ok()?;
468+
found.then_some(button)
469+
}
470+
471+
/// Convert an RmlUi top-origin event position for Spring's bottom-origin mouse
472+
/// API. The relative Rml capture itself retains top-origin coordinates.
473+
fn engine_mouse_position(interface: &NativeInterfaceRef, x: i32, y: i32) -> Option<(i32, i32)> {
474+
let geometry = interface.display().get_view_geometry().ok()?;
475+
Some((x, geometry.viewSizeY - y - 1))
476+
}

‎native/src/sbc/panels/fields/color.rs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use spring_native::{
44
};
55

66
use crate::sbc::panels::field::{
7-
element_by_id, escape_rml, on_change, on_pointer, ChangeQueue, Field, FieldValue,
8-
InteractionQueue,
7+
element_by_id, escape_rml, on_change, on_numeric_pointer, on_pointer, ChangeQueue, Field,
8+
FieldValue, InteractionQueue,
99
};
1010

1111
const CHANNEL_STEP: f32 = 0.005; // 1/200 per pixel, matches original
@@ -272,11 +272,14 @@ impl Field for ColorField {
272272
on_change(interface, e, format!("{}-hex", self.name), changes)?;
273273
}
274274
// Each channel: drag + edit
275+
let (context, has_context) = interface.rml_ui().document_get_context(document)?;
275276
for (i, ch) in ['r', 'g', 'b'].iter().enumerate() {
276277
let sub_name = format!("{}-{ch}", self.name);
277278
if let Some(e) = element_by_id(interface, document, &format!("field-{n}-{ch}-display"))
278279
{
279-
on_pointer(interface, e, sub_name.clone(), interactions)?;
280+
if has_context {
281+
on_numeric_pointer(interface, context, e, sub_name.clone(), interactions)?;
282+
}
280283
}
281284
if let Some(e) = self.channels[i].edit {
282285
on_change(interface, e, sub_name, changes)?;

0 commit comments

Comments
 (0)