First slice of the own positioning engine (replacing @floating-ui's math). Pure, no-DOM foundation, not wired yet: - engine/types.ts — internal positioning types (Rect/Coords/ElementRects/ ClippingContext/MiddlewareState/MiddlewareReturn/Middleware/ComputePosition*). Read-phase model: one coalesced dom.measure reads rects + the full clipping ancestor-walk + arrow dims upfront; middleware then run purely. - engine/geometry.ts — pure placement/coords math (getSide/getAlignment, getOppositePlacement, getOppositeAlignmentPlacement, computeCoordsFromPlacement) reimplemented from the floating-ui algorithm. Compiles clean (0 new errors). The DOM-read core (rects + clipping ancestor-walk) is the substantial next slice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>menubar-v4-safe
parent
e84afb15a7
commit
a3dfe20390
@ -0,0 +1,99 @@
|
||||
// Pure placement / coordinate math. No DOM, no reads — all inputs are rects.
|
||||
// Reimplemented from the floating-ui algorithm (spec), our idioms + types.
|
||||
|
||||
import type { Align, Placement, Side } from '../placement'
|
||||
import { OPPOSITE_SIDE } from '../placement'
|
||||
import type { Axis, Coords, ElementRects, Length, Padding, SideObject } from './types'
|
||||
|
||||
const OPPOSITE_ALIGN: Record<'start' | 'end', 'start' | 'end'> = { start: 'end', end: 'start' }
|
||||
|
||||
export function getSide(placement: Placement): Side {
|
||||
return placement.split('-')[0] as Side
|
||||
}
|
||||
|
||||
export function getAlignment(placement: Placement): Align | undefined {
|
||||
return placement.split('-')[1] as Align | undefined
|
||||
}
|
||||
|
||||
/** The axis the SIDE runs on: left/right → 'x', top/bottom → 'y'. */
|
||||
export function getSideAxis(placement: Placement): Axis {
|
||||
return ['left', 'right'].includes(getSide(placement)) ? 'x' : 'y'
|
||||
}
|
||||
|
||||
/** The axis the ALIGNMENT slides on — perpendicular to the side axis. */
|
||||
export function getAlignmentAxis(placement: Placement): Axis {
|
||||
return getSideAxis(placement) === 'x' ? 'y' : 'x'
|
||||
}
|
||||
|
||||
export function getAxisLength(axis: Axis): Length {
|
||||
return axis === 'y' ? 'height' : 'width'
|
||||
}
|
||||
|
||||
/** Flip the side: bottom ⇄ top, left ⇄ right (alignment preserved). */
|
||||
export function getOppositePlacement(placement: Placement): Placement {
|
||||
const align = getAlignment(placement)
|
||||
const opposite = OPPOSITE_SIDE[getSide(placement)]
|
||||
return (align ? `${opposite}-${align}` : opposite) as Placement
|
||||
}
|
||||
|
||||
/** Flip the alignment: start ⇄ end (side preserved). Center (no suffix) unchanged. */
|
||||
export function getOppositeAlignmentPlacement(placement: Placement): Placement {
|
||||
const align = getAlignment(placement)
|
||||
if (align !== 'start' && align !== 'end') return placement
|
||||
return `${getSide(placement)}-${OPPOSITE_ALIGN[align]}` as Placement
|
||||
}
|
||||
|
||||
export function getPaddingObject(padding: Padding): SideObject {
|
||||
return typeof padding === 'number'
|
||||
? { top: padding, right: padding, bottom: padding, left: padding }
|
||||
: { top: 0, right: 0, bottom: 0, left: 0, ...padding }
|
||||
}
|
||||
|
||||
/**
|
||||
* The base coordinates for a placement — the floating element pinned to the
|
||||
* reference per side + alignment, BEFORE any middleware (offset/shift/flip).
|
||||
* Mirrors floating-ui's `computeCoordsFromPlacement`.
|
||||
*/
|
||||
export function computeCoordsFromPlacement(
|
||||
rects: ElementRects,
|
||||
placement: Placement,
|
||||
rtl = false
|
||||
): Coords {
|
||||
const { reference, floating } = rects
|
||||
const sideAxis = getSideAxis(placement)
|
||||
const alignmentAxis = getAlignmentAxis(placement)
|
||||
const alignLength = getAxisLength(alignmentAxis)
|
||||
const side = getSide(placement)
|
||||
const isVertical = sideAxis === 'y'
|
||||
|
||||
const commonX = reference.x + reference.width / 2 - floating.width / 2
|
||||
const commonY = reference.y + reference.height / 2 - floating.height / 2
|
||||
const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2
|
||||
|
||||
let coords: Coords
|
||||
switch (side) {
|
||||
case 'top':
|
||||
coords = { x: commonX, y: reference.y - floating.height }
|
||||
break
|
||||
case 'bottom':
|
||||
coords = { x: commonX, y: reference.y + reference.height }
|
||||
break
|
||||
case 'right':
|
||||
coords = { x: reference.x + reference.width, y: commonY }
|
||||
break
|
||||
case 'left':
|
||||
coords = { x: reference.x - floating.width, y: commonY }
|
||||
break
|
||||
default:
|
||||
coords = { x: reference.x, y: reference.y }
|
||||
}
|
||||
|
||||
const align = getAlignment(placement)
|
||||
if (align === 'start') {
|
||||
coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1)
|
||||
} else if (align === 'end') {
|
||||
coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1)
|
||||
}
|
||||
|
||||
return coords
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
// Engine-internal positioning types. Reimplemented to our idioms (floating-ui as
|
||||
// spec). The public `Middleware`/`MiddlewareData` in ../types are the consumer
|
||||
// surface; these are the math types the engine threads internally.
|
||||
|
||||
import type { ActiveDom } from '$adom'
|
||||
import type { Placement, Strategy } from '../placement'
|
||||
import type { MiddlewareData, Measurable } from '../types'
|
||||
|
||||
export type Axis = 'x' | 'y'
|
||||
export type Length = 'width' | 'height'
|
||||
|
||||
export type Coords = { x: number; y: number }
|
||||
export type Dimensions = { width: number; height: number }
|
||||
export type Rect = Coords & Dimensions
|
||||
export type SideObject = { top: number; right: number; bottom: number; left: number }
|
||||
export type Padding = number | Partial<SideObject>
|
||||
|
||||
export type ElementRects = { reference: Rect; floating: Rect }
|
||||
|
||||
export type FloatingElements = {
|
||||
reference: Measurable | HTMLElement
|
||||
floating: HTMLElement
|
||||
}
|
||||
|
||||
/**
|
||||
* The clipping region read ONCE upfront — the intersection of every overflow
|
||||
* ancestor's clip + the viewport (the full ancestor-walk, option 2), expressed
|
||||
* in viewport coordinates. `detectOverflow` then compares the floating rect (at
|
||||
* the proposed coords) against this purely, with no further DOM read.
|
||||
*/
|
||||
export type ClippingContext = {
|
||||
/** Floating element's clip: ∩ of its overflow ancestors' clips + viewport. */
|
||||
readonly floating: Rect
|
||||
/** Reference element's clip (only read when a `hide` middleware is active). */
|
||||
readonly reference?: Rect
|
||||
}
|
||||
|
||||
/** State handed to each middleware. All layout already read into `rects` +
|
||||
* `clipping` + `arrowDimensions`; middleware are pure (no DOM read). */
|
||||
export type MiddlewareState = {
|
||||
x: number
|
||||
y: number
|
||||
readonly initialPlacement: Placement
|
||||
placement: Placement
|
||||
readonly strategy: Strategy
|
||||
readonly middlewareData: MiddlewareData
|
||||
readonly rects: ElementRects
|
||||
readonly elements: FloatingElements
|
||||
readonly clipping: ClippingContext
|
||||
readonly arrowDimensions?: Dimensions
|
||||
readonly dom: ActiveDom
|
||||
}
|
||||
|
||||
export type MiddlewareReturn = {
|
||||
x?: number
|
||||
y?: number
|
||||
data?: Record<string, unknown>
|
||||
/** Restart the chain (flip): recompute base coords for the new placement. */
|
||||
reset?: { placement?: Placement }
|
||||
}
|
||||
|
||||
export type Middleware = {
|
||||
readonly name: string
|
||||
readonly options?: unknown
|
||||
readonly fn: (state: MiddlewareState) => MiddlewareReturn
|
||||
}
|
||||
|
||||
export type ComputePositionConfig = {
|
||||
readonly placement?: Placement
|
||||
readonly strategy?: Strategy
|
||||
readonly middleware?: readonly Middleware[]
|
||||
readonly dom: ActiveDom
|
||||
}
|
||||
|
||||
export type ComputePositionReturn = {
|
||||
readonly x: number
|
||||
readonly y: number
|
||||
readonly placement: Placement
|
||||
readonly strategy: Strategy
|
||||
readonly middlewareData: MiddlewareData
|
||||
}
|
||||
Loading…
Reference in new issue