///|
/// Event modifiers: OnTap, OnSecondaryTap, OnLongPress, OnDoubleTap, OnDrag,
/// OnDragWithFrame, OnTapWithFrame, OnFileDrop, KeyboardShortcut.
///|
pub(all) struct OnTapModifier[Msg] {
message : Msg
}
///|
/// Secondary mouse-button activation. Native desktop backends normalize the
/// context-menu button to `button == 1`; primary taps continue to use 0.
///
/// The modifier is deliberately transparent: it contributes no semantics and
/// no focusability. A context-menu hook is an observer, not a control — making
/// it focusable would insert it into focus traversal, and any semantics role
/// it declared would override the host view's role (a tree row must stay a
/// TreeItem, a plain container must not become a Button).
priv struct OnSecondaryTapModifier[Msg] {
message : Msg
}
///|
fn[Msg] OnSecondaryTapModifier::to_any_modifier(
self : OnSecondaryTapModifier[Msg],
) -> AnyModifier[Msg] {
let message = self.message
{
..noop_modifier(),
identity_kind: "OnSecondaryTap",
event: fn(ctx) {
match ctx.event {
// A secondary tap is a press, not a press/release pair: native
// context menus open on button-down, and the host backends show the
// context menu before the release can be dispatched to the tree.
Pointer(event) if event.button == 1 => {
let inside = ctx.frame.contains(event.position)
let current = view_state_to_pressable_state(ctx.state)
let tap = recognize_press_tap(current, inside, event.phase)
let next_state = pressable_state_to_view_state(
ctx.state,
tap.next_state,
)
let activated = event.phase == Down && inside
let changed = next_state != ctx.state || activated
{
changed,
activated,
focused: false,
// No pointer capture: the tap is delivered on press and owns no
// gesture. Capturing would route later presses (a second
// right-click elsewhere before the release) to this element and
// mis-attribute their context-menu target.
captured: false,
state: Some(next_state),
text_control: None,
dirty: if changed {
ViewPaintDirty
} else {
ViewClean
},
messages: if activated {
[message]
} else {
[]
},
}
}
_ => ViewEventResult::ignored()
}
},
}
}
///|
/// Secondary (right-button) tap **with position and frame**.
///
/// `OnSecondaryTapModifier` carries a fixed message, so a context-menu host
/// cannot learn *where* the user right-clicked and therefore cannot hit-test
/// which item to build a menu for. This variant hands over the pointer point
/// and the child frame, mirroring `OnTapWithFrameModifier`.
///
/// Fires on press (not press/release): native context menus open on
/// button-down, and the host backends show the menu before the release can be
/// dispatched to the tree — same rationale as `OnSecondaryTapModifier`.
priv struct OnSecondaryTapWithFrameModifier[Msg] {
handler : (Point, Rect) -> Msg
}
///|
fn[Msg] OnSecondaryTapWithFrameModifier::to_any_modifier(
self : OnSecondaryTapWithFrameModifier[Msg],
) -> AnyModifier[Msg] {
let handler = self.handler
{
..noop_modifier(),
identity_kind: "OnSecondaryTapWithFrame",
event: fn(ctx) {
match ctx.event {
Pointer(event) if event.button == 1 => {
let inside = ctx.frame.contains(event.position)
let current = view_state_to_pressable_state(ctx.state)
let tap = recognize_press_tap(current, inside, event.phase)
let next_state = pressable_state_to_view_state(
ctx.state,
tap.next_state,
)
let activated = event.phase == Down && inside
let changed = next_state != ctx.state || activated
let frame = match ctx.child_frames {
[cf, ..] => cf
[] => ctx.frame
}
{
changed,
activated,
focused: false,
// No pointer capture — see `OnSecondaryTapModifier`: capturing
// would mis-attribute a second right-click elsewhere.
captured: false,
state: Some(next_state),
text_control: None,
dirty: if changed {
ViewPaintDirty
} else {
ViewClean
},
messages: if activated {
[handler(event.position, frame)]
} else {
[]
},
}
}
_ => ViewEventResult::ignored()
}
},
}
}
///|
pub fn[Msg] OnTapModifier::to_any_modifier(
self : OnTapModifier[Msg],
) -> AnyModifier[Msg] {
let message = self.message
{
..noop_modifier(),
identity_kind: "OnTap",
focusable: true,
semantics: ViewSemanticsInfo::new(role=Button),
semantics_actions: [
ViewSemanticsActionHandler::activate(_ctx => {
ViewSemanticsActionResult::message(message)
}),
],
event: fn(ctx) {
// Only the primary button activates a tap. Host backends now route
// right-button presses into the view tree (so per-target context menus
// can observe them), and a secondary press must not also fire the
// control's primary action. Touch/pen events decode to button 0.
match ctx.event {
Pointer(event) if event.button == 0 => {
let inside = ctx.frame.contains(event.position)
let current = view_state_to_pressable_state(ctx.state)
let tap = recognize_press_tap(current, inside, event.phase)
let next_state = pressable_state_to_view_state(
ctx.state,
tap.next_state,
)
let changed = next_state != ctx.state || tap.activated
{
changed,
activated: tap.activated,
focused: false,
captured: inside || current == Pressed,
state: Some(next_state),
text_control: None,
dirty: if changed {
ViewPaintDirty
} else {
ViewClean
},
messages: if tap.activated {
[message]
} else {
[]
},
}
}
Keyboard(event) =>
if event.pressed && (event.key == "Enter" || event.key == " ") {
ViewEventResult::message(message)
} else {
ViewEventResult::ignored()
}
_ => ViewEventResult::ignored()
}
},
}
}
///|
pub(all) struct OnLongPressModifier[Msg] {
message : Msg
}
///|
pub fn[Msg] OnLongPressModifier::to_any_modifier(
self : OnLongPressModifier[Msg],
) -> AnyModifier[Msg] {
let message = self.message
{
..noop_modifier(),
identity_kind: "OnLongPress",
focusable: true,
semantics: ViewSemanticsInfo::new(role=Button),
event: fn(ctx) {
// Primary button only: see OnTapModifier.
match ctx.event {
Pointer(event) if event.button == 0 => {
let result = LongPressGestureState::new().update(
event,
inside=ctx.frame.contains(event.position),
)
if result.recognized {
ViewEventResult::message(message)
} else {
{
..ViewEventResult::ignored(),
changed: result.cancelled,
captured: result.cancelled,
dirty: if result.cancelled {
ViewPaintDirty
} else {
ViewClean
},
}
}
}
_ => ViewEventResult::ignored()
}
},
}
}
///|
pub(all) struct OnDoubleTapModifier[Msg] {
message : Msg
}
///|
pub fn[Msg] OnDoubleTapModifier::to_any_modifier(
self : OnDoubleTapModifier[Msg],
) -> AnyModifier[Msg] {
let message = self.message
{
..noop_modifier(),
identity_kind: "OnDoubleTap",
focusable: true,
semantics: ViewSemanticsInfo::new(role=Button),
event: fn(ctx) {
// Primary button only: see OnTapModifier.
match ctx.event {
Pointer(event) if event.button == 0 => {
let result = DoubleTapGestureState::new().update(
event,
inside=ctx.frame.contains(event.position),
)
if result.recognized {
ViewEventResult::message(message)
} else {
ViewEventResult::ignored()
}
}
_ => ViewEventResult::ignored()
}
},
}
}
///|
pub(all) struct OnDragModifier[Msg] {
handler : (DragGestureEvent) -> Msg
}
///|
pub fn[Msg] OnDragModifier::to_any_modifier(
self : OnDragModifier[Msg],
) -> AnyModifier[Msg] {
let handler = self.handler
{
..noop_modifier(),
identity_kind: "OnDrag",
semantics: ViewSemanticsInfo::new(role=Button),
event: fn(ctx) {
handle_drag_modifier_event(ctx, handler=fn(event, _frame) {
handler(event)
})
},
}
}
///|
pub(all) struct OnDragWithFrameModifier[Msg] {
handler : (DragGestureEvent, Rect) -> Msg
}
///|
/// Tap-with-position modifier for canvas/custom-drawn surfaces: unlike
/// `OnTapModifier` (fixed message, no geometry) this hands the tap position
/// and the child frame to the handler so the app can hit-test in its own
/// coordinate space (`position - frame.origin` = view-local point).
///
/// Deliberately transparent like `OnSecondaryTapModifier`: a canvas surface
/// is an observer, not a control — a Button semantics role here would
/// override the host view's role and pollute focus traversal.
priv struct OnTapWithFrameModifier[Msg] {
handler : (Point, Rect) -> Msg
}
///|
fn[Msg] OnTapWithFrameModifier::to_any_modifier(
self : OnTapWithFrameModifier[Msg],
) -> AnyModifier[Msg] {
let handler = self.handler
{
..noop_modifier(),
identity_kind: "OnTapWithFrame",
event: fn(ctx) {
// Primary button only: see OnTapModifier.
match ctx.event {
Pointer(event) if event.button == 0 => {
let inside = ctx.frame.contains(event.position)
let current = view_state_to_pressable_state(ctx.state)
let tap = recognize_press_tap(current, inside, event.phase)
let next_state = pressable_state_to_view_state(
ctx.state,
tap.next_state,
)
let activated = tap.activated
let changed = next_state != ctx.state || activated
let frame = match ctx.child_frames {
[cf, ..] => cf
[] => ctx.frame
}
{
changed,
activated,
focused: false,
captured: inside || current == Pressed,
state: Some(next_state),
text_control: None,
dirty: if changed {
ViewPaintDirty
} else {
ViewClean
},
messages: if activated {
[handler(event.position, frame)]
} else {
[]
},
}
}
_ => ViewEventResult::ignored()
}
},
}
}
///|
pub fn[Msg] OnDragWithFrameModifier::to_any_modifier(
self : OnDragWithFrameModifier[Msg],
) -> AnyModifier[Msg] {
let handler = self.handler
{
..noop_modifier(),
identity_kind: "OnDragWithFrame",
semantics: ViewSemanticsInfo::new(role=Button),
event: fn(ctx) { handle_drag_modifier_event(ctx, handler~) },
}
}
///|
fn[Msg] handle_drag_modifier_event(
ctx : ViewEventContext,
handler~ : (DragGestureEvent, Rect) -> Msg,
) -> ViewEventResult[Msg] {
// Primary button only: secondary presses belong to context menus.
match ctx.event {
Pointer(event) if event.button == 0 => {
let slot = drag_gesture_view_state_slot()
let gesture = ctx.state.read_slot(slot)
let result = gesture.update(
event,
inside=ctx.frame.contains(event.position),
)
let active = result.state.phase != Idle
let next_state = if active {
ctx.state.with_slot(slot, result.state)
} else {
ctx.state.without_slot(slot)
}
match result.event {
Some(drag_event) => {
let frame = match ctx.child_frames {
[cf, ..] => cf
[] => ctx.frame
}
{
changed: true,
activated: true,
focused: false,
captured: true,
state: Some(next_state),
text_control: None,
dirty: ViewPaintDirty,
messages: [handler(drag_event, frame)],
}
}
None =>
if active {
{
changed: next_state != ctx.state,
activated: false,
focused: false,
captured: true,
state: Some(next_state),
text_control: None,
dirty: if next_state != ctx.state {
ViewPaintDirty
} else {
ViewClean
},
messages: [],
}
} else {
// Terminal pointer phases must commit the reset even when the
// gesture did not cross the drag slop. In particular, a simple
// click leaves the recognizer in `Pending`; returning `ignored`
// here would preserve that slot forever and the next Move could
// be interpreted as a drag from the old click.
{
// State is still committed, but terminal cleanup is transparent
// to parent tap modifiers so they can observe the same Up.
changed: false,
activated: false,
focused: false,
captured: false,
state: Some(next_state),
text_control: None,
dirty: if next_state != ctx.state {
ViewPaintDirty
} else {
ViewClean
},
messages: [],
}
}
}
}
_ => ViewEventResult::ignored()
}
}
///|
pub(all) struct OnFileDropModifier[Msg] {
handler : (Array[String], Point) -> Msg
}
///|
pub fn[Msg] OnFileDropModifier::to_any_modifier(
self : OnFileDropModifier[Msg],
) -> AnyModifier[Msg] {
let handler = self.handler
{
..noop_modifier(),
identity_kind: "OnFileDrop",
event: fn(ctx) {
match ctx.event {
DragDrop(event) =>
match (event.phase, event.position) {
(Dropped, Some(position)) =>
if ctx.frame.contains(position) && event.paths.length() > 0 {
{
changed: true,
activated: true,
focused: false,
captured: true,
state: None,
text_control: None,
dirty: ViewPaintDirty,
messages: [handler(event.paths, position)],
}
} else {
ViewEventResult::ignored()
}
_ => ViewEventResult::ignored()
}
_ => ViewEventResult::ignored()
}
},
}
}
///|
pub(all) struct KeyboardShortcutModifier {
shortcut : KeyboardShortcut
}
///|
pub fn[Msg] KeyboardShortcutModifier::to_any_modifier(
self : KeyboardShortcutModifier,
) -> AnyModifier[Msg] {
let shortcut = self.shortcut
{
..noop_modifier(),
identity_kind: "KeyboardShortcut",
focusable: true,
semantics: ViewSemanticsInfo::new(role=Button),
event: fn(ctx) { handle_keyboard_shortcut_modifier(shortcut, ctx) },
}
}
///|
fn[Msg] handle_keyboard_shortcut_modifier(
shortcut : KeyboardShortcut,
ctx : ViewEventContext,
) -> ViewEventResult[Msg] {
match ctx.event {
Keyboard(event) =>
if shortcut.matches(event) {
{ ..ViewEventResult::ignored(), activated: true, captured: true, }
} else {
ViewEventResult::ignored()
}
_ => ViewEventResult::ignored()
}
}