Skip to content

Hits Design

Only one element in the app can react to the mouse. The Hits spatial index knows which one. It's the single source of truth for hover and click dispatch. Consistent behavior everywhere.

For timing logic (autorepeat, long-click, double-click), see timers.md.

Table of Contents


Overview & Status

Overview

Centralized click handling using the Hits spatial index to dispatch handle_s_mouse to the component under the mouse. Eliminates per-component DOM event handlers.

Status

  • [x] Complete — Core hit detection and dispatch
  • [x] Complete — All mouse timing centralized (see timers.md)
  • [ ] Remaining work
    • [ ] Search_Results.svelte — complex (dynamic rows, may be too granular)
    • [ ] breadcrumb button — not yet migrated to centralized autorepeat
    • [ ] close button — does not yet use Button

Benefits

  • Single source of truth: one manager dispatches all clicks and hover
  • Consistent precedence: dots > widgets > rings > controls > rubberband
  • Cleaner components: register handler, receive callbacks
  • Consistent behavior: all hovering and clicking works the same way

Architecture

Hits Manager

The manager is the single point of truth regarding which element is reactive to hover and click. It uses the bounding rects of ALL registered elements to determine which one contains the current mouse position. It then calls handle_s_mouse on that element for mouse up and down events, and sets hits.w_s_hover for mouse entering or leaving the bounding rect.

Some elements have a shape very different than a rectangle. S_Hit_Target provides an optional hook that can refine the enter/leave boundary.

Click Detection Flow

On mousedown/mouseup at the document level (Events.ts):

  1. Call hits.targets_atPoint(point) to find targets under cursor
  2. Select topmost target using priority: dot → widget → ring → control → other
  3. Invoke target.handle_s_mouse(s_mouse) if defined
  4. Hits handles timing centrally (see timers.md)
mousedown → Events.ts → hits.handle_click_at(point, s_mouse)

                     targets_atPoint(point)

                     select topmost target

                     target.handle_s_mouse(s_mouse)

S_Hit_Target

The superclass of all element and component UX state objects (S_Element and S_Component).

Hit Rect

Hits uses a highly performant RBush index that takes a mouse position (x, y) and returns hit targets enclosing that point. Each hit target is assigned a rect. The rect must be kept current — updated when graph is altered or details toggled.

Registration:

The rect setter always calls hits.add_hit_target(this) unconditionally:

ts
set rect(value: Rect | null) {
    this.element_rect = value;
    hits.add_hit_target(this);  // Always called
}

Clipping:

Hit rects for graph elements (dots, widgets, rings) are clipped to the visible graph view:

ts
update_rect() {
    if (!!this.html_element) {
        let rect = g.scaled_rect_forElement(this.html_element);
        if (rect && (this.isADot || this.isAWidget || this.isRing)) {
            const graph_bounds = get(g.w_rect_ofGraphView);
            if (graph_bounds) {
                rect = rect.clippedTo(graph_bounds);
            }
        }
        this.rect = rect;
    }
}

Click Handler

Optional handle_s_mouse method:

ts
handle_s_mouse?: (s_mouse: S_Mouse) => boolean;

Component Pattern

Components register a handler on their hit target:

ts
s_element.handle_s_mouse = (s_mouse: S_Mouse): boolean => {
    // handle click, return true if consumed
    return handle_s_mouse(s_mouse);
};

DOM on:mouse* handlers are removed — only centralized Events.ts listeners remain.

S_Mouse

A transient value object encapsulating current mouse-relevant information:

  • What happened: isDown, isUp, isLong, isDouble, isRepeat, isMove
  • Where: element (the target HTMLElement)
  • Raw data: event (the original MouseEvent)

Static factories:

ts
S_Mouse.down(event, element)    // user pressed
S_Mouse.up(event, element)      // user released
S_Mouse.long(event, element)    // held past threshold
S_Mouse.double(event, element)  // second click within threshold
S_Mouse.repeat(event, element)  // autorepeat tick

Deprecated patterns:

OldNew
elements.s_mouse_forName(name)Fresh S_Mouse instances each time
S_Mouse.clicksS_Hit_Target.clicks
detect_autorepeat booleanmouse_detection enum

Migration Guide

Step 1: Add S_Element (for components without one)

ts
const s_element = elements.s_element_for(new Identifiable(name), T_Hit_Target.button, name);
let element: HTMLElement;

onMount(() => {
    s_element.set_html_element(element);
    s_element.handle_s_mouse = handle_s_mouse;
});

onDestroy(() => {
    hits.delete_hit_target(s_element);
});
svelte
<div bind:this={element}>...</div>

Step 2: Set handle_s_mouse (for components with existing S_Element)

ts
s_element.handle_s_mouse = (s_mouse: S_Mouse): boolean => {
    return handle_s_mouse(s_mouse);
};

Step 3: Remove DOM handlers

diff
- on:pointerdown={handle_pointerDown}
- on:pointerup={handle_pointerUp}

Component Status

Migrated ✅

ComponentNotes
Button.svelteS_Element + handle_s_mouse, supports all timing modes
Glow_Button.svelteS_Element + handle_s_mouse + hover via hits
Next_Previous.svelteArray of S_Elements for multiple buttons
Widget_Title.sveltes_title + s_widget handle_s_mouse
Radial_Rings.sveltes_rotation + s_resizing handle_s_mouse
Cluster_Pager.sveltes_pager handle_s_mouse for thumbs
Radial_Cluster.sveltes_paging handle_s_mouse for paging arcs
Rubberband.svelteCatch-all for empty graph space
Close_Button.svelteFixed handler setup and RBush entry management
Widget_Drag.svelteResponds on isDown
Widget_Reveal.svelteResponds on isDown
Segmented.svelteChanged to on:mousedown
Breadcrumb_Button.svelteResponds on isDown
D_Actions.svelteAll actions autorepeat
Steppers.sveltePassed to Triangle_Button
Triangle_Button.svelteWraps Button
Buttons_Row.svelteUses Button
Buttons_Table.svelteUses Buttons_Row

Pending

ComponentIssue
Search_Results.svelteEach row would need its own S_Element — too granular

Rubberband

Rubberband handles clicks on "empty" graph space. Registers directly as a catch-all hit target instead of Graph.svelte delegating.

Why Rubberband, not Graph?

  • Rubberband actually needs the click
  • Graph just passes through — unnecessary middleman
  • Rubberband already has bounds prop

Implementation:

ts
const s_element = elements.s_element_for(new Identifiable('rubberband'), T_Hit_Target.rubberband, 'graph');

onMount(() => {
    s_element.set_html_element(rubberband_hit_area);
    s_element.handle_s_mouse = handle_s_mouse;
});
svelte
<div class='rubberband-hit-area' bind:this={rubberband_hit_area}
     style='position:absolute; top:0; left:0; width:{bounds.size.width}px; height:{bounds.size.height}px; pointer-events:none;'/>

Meta key forces rubberband target for graph dragging.


Testing

Setup

All tests assume a widget is selected in the graph or list view.

Terms

TermDefinition
details panelStack of buttons opening panels. Tap details toggle (three bars, top left) to show.
actions panelDetails panel showing action buttons in seven categories.
re-renderSvelte destroys/recreates component. S_Hit_Target persists.

Regression

  1. Normal-click buttons work normally
    • [x] Single click works, no delays or repeats
    • [x] breadcrumbs — fixed (responds on isDown)
    • [x] details toggle — fixed (responds on isDown)
    • [x] search — fixed (handler setup improved)
    • [x] close search — fixed (handler setup, removed stale RBush entries)
    • [x] widget drag/reveal buttons — fixed (respond on isDown)
    • [x] segmented controls — fixed (changed to on:mousedown)

For timing tests (autorepeat, double-click, long-click), see timers.md.


Reference

Hit Target Type Getters

GetterIncludes
isAControlT_Hit_Target.control, T_Hit_Target.button
isAWidgetT_Hit_Target.widget, T_Hit_Target.title
isRingT_Hit_Target.ring, T_Hit_Target.paging
isADotT_Hit_Target.dot

Component Complexity

ComponentNotes
Rubberband.svelteCatch-all, lowest priority
Search_Results.svelteToo granular for per-row S_Element
Breadcrumb_Button.svelteDual state: S_Widget for colors, separate S_Element for hit target