Calculating Timeline Geometry for UI Rendering in PalmierPro: A Deep Dive into the TimelineGeometry Struct
PalmierPro calculates timeline geometry for UI rendering using a stateless TimelineGeometry struct that performs pure mathematical conversions between frame/track indices and screen coordinates, enabling deterministic hit-testing and rendering.
The PalmierPro video editor renders its non-linear timeline entirely in Swift, requiring precise calculations to map logical timeline data (frames, tracks, clips) onto screen coordinates. According to the palmier-io/palmier-pro source code, the heavy lifting is centralized in the TimelineGeometry struct located at Sources/PalmierPro/Timeline/TimelineGeometry.swift, which separates pure geometry calculations from rendering and input handling.
The Stateless Architecture of TimelineGeometry
TimelineGeometry is deliberately stateless, storing only the values required to define the current layout. This design ensures that the same struct can be shared between the drawing code in TimelineView and the input controller (TimelineInputController), guaranteeing that hit-testing and rendering remain perfectly synchronized during scroll or zoom operations.
The struct initializes with these core properties:
pixelsPerFrame– Horizontal scale (pixels per frame) derived from the editor's zoom levelheaderWidth– Width of the optional left-hand header for track lists and thumbnailsrulerHeight– Fixed vertical height of the time-code ruler (defined asLayout.rulerHeight)trackHeights– Height of each individual track (typicallyLayout.trackHeight)bounds– The view's clipping rectangle used for scroll calculations
During initialization, TimelineGeometry pre-computes a cumulative Y array (cumulativeY) that stores the top-edge Y-coordinate of each track. This optimization makes every track-to-Y lookup O(1) instead of O(n), critical for performance during rapid mouse movements and redraws.
Converting Between Frames and Pixels
The primary responsibility of calculating timeline geometry for UI rendering involves bidirectional conversion between logical frame indices and horizontal pixel positions.
frameAt(x:) converts a horizontal pixel coordinate to a frame index:
let frame = geometry.frameAt(x: mouseX) // pixel → frame index
xForFrame(_:) performs the inverse operation, returning the pixel X-coordinate for a given frame:
let xPos = geometry.xForFrame(frame) // frame index → pixel
These methods serve as the foundation for hit-testing in TimelineInputController, determining precisely which frame the cursor points at during scrubbing or clip positioning.
Mapping Tracks to Y Coordinates
Vertical positioning follows a similar pattern using the pre-computed cumulative array.
trackAt(y:) determines which track index contains a specific Y-coordinate:
let trackIdx = geometry.trackAt(y: mouseY) // pixel → track index
trackY(at:) retrieves the top-edge Y-coordinate for a specific track:
let yTop = geometry.trackY(at: trackIdx) // track index → top-edge Y
Because the struct maintains the cumulativeY array, these lookups execute in constant time regardless of track count, ensuring smooth performance even with complex timelines containing dozens of tracks.
Calculating Clip Rectangles for Rendering
When TimelineView needs to draw clips, it relies on TimelineGeometry to calculate exact screen rectangles.
clipRect(for:trackIndex:) returns the precise rectangle for a clip on a specific track:
let rect = geometry.clipRect(for: clip, trackIndex: trackIdx)
ClipRenderer.draw(clip,
type: clip.mediaType,
in: rect,
isSelected: isSelected,
context: ctx,
cache: editor.mediaVisualCache)
For more generic positioning, clipRect(for:atY:height:) accepts explicit Y-coordinates and heights, enabling the rendering of ghost clips and temporary overlays outside standard track layouts.
Geometry for Drag-and-Drop Operations
Calculating timeline geometry for UI rendering becomes particularly complex during drag-and-drop interactions, where the system must determine valid drop zones and render visual feedback.
dropTargetAt(y:) analyzes a Y-coordinate and returns a TrackDropTarget enum indicating whether the user is hovering over an existing track or a potential new track insertion point:
let target = geometry.dropTargetAt(y: dragY)
switch target {
case .newTrackAt(let index):
// Show insertion line at geometry.insertionLineY(for: target)
case .existingTrack(let index):
// Highlight the existing track
}
insertionLineY(for:) calculates the exact Y-coordinate for drawing the visual insertion line when creating new tracks:
if let lineY = geometry.insertionLineY(for: dropTarget) {
ctx.setStrokeColor(NSColor.systemYellow.cgColor)
ctx.setLineWidth(2)
ctx.move(to: CGPoint(x: 0, y: Double(lineY)))
ctx.addLine(to: CGPoint(x: Double(bounds.width), y: Double(lineY)))
ctx.strokePath()
}
ghostY(for:height:) determines the Y-position for "ghost" clips—semi-transparent representations of clips being dragged to new track drop zones:
if let ghostY = geometry.ghostY(for: dragTarget) {
let ghostRect = geometry.clipRect(for: ghostClip,
atY: Double(ghostY),
height: Layout.trackHeight)
// draw the ghost with reduced opacity
}
UI Constants and Design System Integration
The geometric calculations rely on centralized layout constants defined in Sources/PalmierPro/UI/Layout.swift, including rulerHeight, dropZoneHeight, insertThreshold, and trackHeight. These values form part of PalmierPro's broader design system, which also encompasses AppTheme.swift for colors, spacing, and fonts.
Because all measurements derive from these constants, the geometry engine remains consistent with the application's visual design while allowing for easy theming and layout adjustments.
Summary
TimelineGeometryinSources/PalmierPro/Timeline/TimelineGeometry.swiftprovides pure, side-effect-free calculations for mapping timeline data to screen coordinates.- The struct maintains a cumulative Y array for O(1) track-to-coordinate lookups, optimizing performance during interaction.
- Bidirectional conversion methods (
frameAt(x:)/xForFrame(_:)andtrackAt(y:)/trackY(at:)) synchronize rendering and input handling. - Drop-target detection (
dropTargetAt(y:)) and associated geometry methods (insertionLineY,ghostY) enable sophisticated drag-and-drop interactions. - Layout constants in
Sources/PalmierPro/UI/Layout.swiftensure geometric calculations align with the application's design system.
Frequently Asked Questions
How does PalmierPro handle coordinate conversion when the user zooms the timeline?
When the zoom level changes, TimelineView creates a new TimelineGeometry instance with an updated pixelsPerFrame value derived from the editor's current zoom state. Because the struct is stateless and recalculated on each frame, all subsequent coordinate conversions automatically reflect the new scale without requiring manual invalidation of cached values.
Why is TimelineGeometry designed as a struct rather than a class?
TimelineGeometry uses a struct to enforce value semantics and immutability. This design guarantees that the geometry used during the rendering phase matches exactly the geometry used for hit-testing, preventing synchronization bugs when the timeline scrolls or zooms between input processing and screen drawing.
Where does PalmierPro store the height constants for tracks and rulers?
Height constants such as trackHeight, rulerHeight, and dropZoneHeight reside in Sources/PalmierPro/UI/Layout.swift. These static values provide the default measurements that TimelineGeometry uses when calculating cumulative Y positions and clip rectangles.
How does the geometry engine support creating new tracks via drag-and-drop?
The dropTargetAt(y:) method detects when the user drags a clip to an area between existing tracks, returning a TrackDropTarget.newTrackAt(index) case. The view then uses insertionLineY(for:) to draw a visual indicator and ghostY(for:height:) to position a preview of the clip before the user releases the mouse, providing immediate visual feedback about the new track creation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →