TimelineScale.h header

#include <ew/app/TimelineScale.h>

Namespace ew::app

TimelineMinimapRect struct

struct ew::app::TimelineMinimapRect

The viewport rectangle drawn on a minimap strip, in strip pixels.

Members

int ew::app::TimelineMinimapRect::left = 0

The rectangle's left edge, in strip pixels.

int ew::app::TimelineMinimapRect::right = 0

The rectangle's right edge, in strip pixels.

TimelineScale class

class ew::app::TimelineScale

The mapping between story time and screen pixels for a timeline view: which span is on screen, how far it is zoomed, and where any instant lands. Pure and UI-free, so the rules that decide what a timeline can show are testable without a widget – the desktop panel owns pixels and input, this owns time.

Two pieces of state say everything: the instant at x = 0 (viewStart) and how much time one pixel covers (minutesPerPixel). Everything else is derived.

Zoom is expressed as time-per-pixel, never as a multiple of the data's span. That distinction is the whole point (docs/TIMELINE_UX.md TL-05, TL-20): the old view fitted its axis to the events and capped zoom at 64x of that span, so a project with a 3,000-year backstory could never see finer than ~47 years per screen – the eleven days in which its plot happened were unreachable at any setting (docs/TIMELINE_AUDIT.md TA-4). Limits in minutes-per-pixel are absolute, so a five-minute duel and a five-thousand-year age are equally reachable in the same view.

The viewport is a window onto an unbounded axis (TL-03): it is never clamped to the events, so a user can look before the first event or after the last one and see where a new one would go. A scrollbar's range is therefore a separate question (the data's extent united with the view), and is not this class's business.

Members

double ew::app::TimelineScale::kFinestMinutesPerPixel = 0.05

Minutes one pixel may cover at the deepest zoom – 0.05 puts twenty pixels on a minute, which is finer than any story needs and leaves headroom below "a scene".

double ew::app::TimelineScale::kCoarsestMinutesPerPixel = 1.0e7

Minutes one pixel may cover at the shallowest zoom. Ten million minutes is about nineteen years per pixel, so a full-width view still spans tens of thousands of years – more than any world's recorded history, and far past where eliding empty span (TL-23) takes over anyway.

void ew::app::TimelineScale::setViewportWidth(int pixels)

The width in pixels of the plot area the scale maps onto. Values below one are treated as one, so a zero-width widget during construction cannot divide by zero.

int ew::app::TimelineScale::viewportWidth() const

The plot width in pixels; at least 1.

qint64 ew::app::TimelineScale::viewStart() const

The instant at the left edge of the view (x = 0), in minutes since the story epoch. May be negative and is deliberately not clamped to any content.

void ew::app::TimelineScale::setViewStart(qint64 instant)

Sets the instant at the left edge of the view (viewStart); not clamped to content.

double ew::app::TimelineScale::minutesPerPixel() const

How much story time one pixel covers. Always within [kFinestMinutesPerPixel, kCoarsestMinutesPerPixel].

void ew::app::TimelineScale::setMinutesPerPixel(double minutesPerPixel)

Sets the zoom, clamped to the limits. Keeps viewStart fixed, so the left edge stays put; use zoomAt to keep a chosen point fixed instead.

qint64 ew::app::TimelineScale::viewEnd() const

The instant at the right edge of the view.

qint64 ew::app::TimelineScale::visibleMinutes() const

The span the view currently covers, in minutes.

double ew::app::TimelineScale::xForInstant(qint64 instant) const

The x offset, in pixels from the plot's left edge, at which instant sits. Not clamped: an instant off-screen returns an x outside [0, viewportWidth), which is what a caller culling to the viewport needs to know.

qint64 ew::app::TimelineScale::instantForX(double x) const

The instant at x pixels from the plot's left edge. The exact inverse of xForInstant at pixel granularity.

void ew::app::TimelineScale::panPixels(double pixels)

Scrolls the view right by pixels (negative scrolls left), leaving the zoom alone.

void ew::app::TimelineScale::zoomAt(double anchorX, double factor)

Zooms about anchorX – the pixel under the pointer – multiplying the magnification by factor (greater than one zooms in). **The instant at anchorX does not move.** This is the whole contract: zooming about the centre, or about the axis's start, means every zoom has to be undone by a pan, which users experience as the view fighting them (TL-21). The audit found the old view changed its zoom without touching its scroll at all, so it always zoomed towards the beginning of the timeline no matter where the pointer was (TA-6).

A factor of zero or less is ignored. At a zoom limit the anchor still holds – the view simply stops magnifying.

void ew::app::TimelineScale::fitTo(qint64 first, qint64 last, int paddingPixels)

Frames [first, last] in the viewport with paddingPixels of margin at each side, so "show me everything" and "show me exactly this" are one operation (TL-22). A reversed or empty range is framed around its midpoint at the current zoom rather than dividing by zero.

bool ew::app::TimelineScale::canZoomIn() const

True when zooming in (or out) any further would exceed a limit, so a view can disable its controls honestly rather than offering a button that does nothing.

bool ew::app::TimelineScale::canZoomOut() const

True when zooming out any further would exceed the coarsest-zoom limit.

TimelineScrollRange struct

struct ew::app::TimelineScrollRange

The span a horizontal scrollbar should cover, and whether one is warranted at all.

Members

qint64 ew::app::TimelineScrollRange::first = 0

The earliest instant the scrollbar spans.

qint64 ew::app::TimelineScrollRange::last = 0

The latest instant the scrollbar spans.

bool ew::app::TimelineScrollRange::scrollable = false

False when the whole span is already on screen. TL-36b wants the bar to appear only when something is off-screen: its absence is itself the statement "this is all of it", and a bar that is always present says nothing.

Functions

int ew::app::timelineLaneHeight(int availableHeight, int trackCount, int minHeight)

The height each of trackCount lanes gets so they divide availableHeight evenly and fill it with no dead space below the last track (TL-44's lanes-use-the-space intent), never shrinking a lane below minHeight. When the lanes would overflow – too many tracks for the space – each keeps minHeight and the surplus is clipped, since the timeline does not scroll vertically. With no tracks the whole area is treated as one notional lane. Negative inputs are floored at zero / minHeight so a mid-resize transient can never produce a nonsense height.

qint64 ew::app::timelineMinimapInstant(const TimelineScrollRange &span, int stripWidth, double x)

The instant at strip x x – the inverse of timelineMinimapX, which is what dragging the viewport rect is built on. A non-finite x yields the span's start rather than nonsense.

TimelineMinimapRect ew::app::timelineMinimapRect(const TimelineScrollRange &span, int stripWidth, qint64 viewStart, qint64 viewEnd, int minWidth)

The viewport rect for [viewStart, viewEnd], never narrower than minWidth and always wholly inside the strip.

The floor is the point, not a detail: zoomed into one day of a 3,000-year world the view is a ten-thousandth of the span, which rounds to nothing. A rect you cannot see is a rect you cannot grab, and TL-37 asks for one you can drag. It widens about the view's middle and then slides the result inside the strip – sliding rather than clipping, so the target stays a constant, whole, grabbable size even at the very ends.

double ew::app::timelineMinimapX(const TimelineScrollRange &span, int stripWidth, qint64 instant)

The strip x at which instant sits, on a stripWidth -wide strip showing span.

The minimap and the scrollbar take the SAME span – timelineScrollRange's content-united- with-view – so the two can never disagree about where the user is or how much exists. They are one answer at two resolutions: the bar states the ratio, the strip draws the shape (TL-37 makes the strip double as the density map of TL-32).

Returns an UNCLAMPED x on purpose: an instant outside the span is off the strip, and a caller drawing a density mark needs to know that in order to cull it rather than pile every stray event onto the edge.

TimelineScrollRange ew::app::timelineScrollRange(const TimelineScale &scale, qint64 contentStart, qint64 contentEnd, bool hasContent)

The span a scrollbar should cover for scale over content spanning [contentStart, contentEnd] – the content's extent united with the current view.

United, and not simply the extent, because the axis is unbounded (TL-03): the view may sit wholly outside the content, and a bar spanning only the content could neither show where the user actually is nor let them drag back to their data.

hasContent is passed rather than inferred from contentEnd > contentStart, because those are equal for a perfectly ordinary timeline – one whose events are all instants at the same moment. Conflating "no events" with "no extent" is what blanked the panel once already; see TimelinePanel::frameIfNeeded.

This is a free function, not a member, because it is exactly what TimelineScale is not: the scale knows only where the view is, and deliberately never learns what content exists.