Skip to main content

max / makeover-layout

10.0 KB · 282 lines History Blame Raw
1 // Names this module's prose links to, resolved for rustdoc.
2 #[allow(unused_imports)]
3 use crate::{Region, Share, Tone};
4
5 /// What a [`Track`]'s integers count.
6 ///
7 /// `Track::fraction` never needed this -- the arithmetic is the same whatever
8 /// the numbers mean -- which is exactly how the ruler came to assume minutes
9 /// and print `00:00` over a month. A renderer drawing an axis has to write a
10 /// label, and it cannot derive the unit from the numbers.
11 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12 #[non_exhaustive]
13 pub enum Unit {
14 /// Minutes from the start of a day. A day view.
15 #[default]
16 Minutes,
17 /// Whole days. A month strip, a sprint, a stretch of leave.
18 ///
19 /// A day-granularity axis is a *strip*, not a calendar: one line with
20 /// spans laid along it. What it deliberately does not do is wrap into
21 /// weeks, which is the shape that makes weekday periodicity visible and
22 /// the one job of a month grid that a strip cannot take over. See the
23 /// crate header.
24 Days,
25 }
26
27 /// A window on an axis, in whatever [`Unit`] its [`Track`] counts.
28 ///
29 /// The axis a [`Track`] draws. Offsets rather than instants, because a
30 /// description carrying a `DateTime` would carry a timezone with it and the
31 /// vocabulary has no business holding one. The app knows which day or month
32 /// this is; the description says how far along it a thing sits.
33 ///
34 /// `to` is exclusive and may exceed the natural period, which is how a span
35 /// running past the end is said without a second date: under
36 /// [`Unit::Minutes`], `Span::new(1320, 1560)` is 22:00 to 02:00.
37 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38 pub struct Span {
39 from: u16,
40 to: u16,
41 }
42
43 impl Span {
44 /// Midnight to midnight, the ordinary day.
45 pub const DAY: Self = Self { from: 0, to: 1440 };
46
47 /// A span, clamped to a sane one.
48 ///
49 /// An empty or backwards span is a caller bug that should not cost a
50 /// renderer a division by zero, so `to` is forced at least one minute past
51 /// `from` rather than returning an error nobody can act on. Same reasoning
52 /// as [`Share::percent`], which clamps rather than refuses.
53 #[must_use]
54 pub const fn new(from: u16, to: u16) -> Self {
55 Self {
56 from,
57 to: if to > from { to } else { from + 1 },
58 }
59 }
60
61 /// The first minute on the axis.
62 #[must_use]
63 pub const fn from(self) -> u16 {
64 self.from
65 }
66
67 /// One past the last minute on the axis.
68 #[must_use]
69 pub const fn to(self) -> u16 {
70 self.to
71 }
72
73 /// How much the axis covers, in its track's unit. Never zero.
74 #[must_use]
75 pub const fn length(self) -> u16 {
76 self.to - self.from
77 }
78
79 /// Whether an offset falls on this axis.
80 #[must_use]
81 pub const fn holds(self, minute: u16) -> bool {
82 minute >= self.from && minute < self.to
83 }
84 }
85
86 impl Default for Span {
87 fn default() -> Self {
88 Self::DAY
89 }
90 }
91
92 /// Where a thing sits on a [`Track`], and for how long.
93 ///
94 /// The one fact a list cannot carry and the whole reason this primitive exists.
95 /// A list says what order things come in; a track says a thing starts 135
96 /// minutes along and lasts 45, which is a different claim and not derivable
97 /// from the first.
98 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99 pub struct Placement {
100 at: u16,
101 length: u16,
102 }
103
104 impl Placement {
105 /// A placement, clamped to a drawable one.
106 ///
107 /// Zero length becomes one for the same reason [`Span::new`] clamps: a
108 /// zero-height thing is invisible rather than expressive, and every
109 /// renderer would need its own guard.
110 #[must_use]
111 pub const fn new(at: u16, length: u16) -> Self {
112 Self {
113 at,
114 length: if length == 0 { 1 } else { length },
115 }
116 }
117
118 /// Offset from the axis origin, matching [`Span`]'s.
119 #[must_use]
120 pub const fn at(self) -> u16 {
121 self.at
122 }
123
124 /// How long it lasts, in its track's unit. Never zero.
125 #[must_use]
126 pub const fn length(self) -> u16 {
127 self.length
128 }
129
130 /// One past its last minute.
131 #[must_use]
132 pub const fn end(self) -> u16 {
133 self.at + self.length
134 }
135
136 /// Whether two placements cover any of the same time.
137 ///
138 /// Geometry, and deliberately not a described field. Whether an overlap is
139 /// a *conflict* is the app's judgment -- a meeting inside a block of free
140 /// time overlaps and is fine -- and that judgment travels the way every
141 /// other judgment does, as a [`Tone`] on the thing itself. What a renderer
142 /// needs in order to lay two things side by side instead of on top of each
143 /// other is this, and it can compute it.
144 ///
145 /// The alternative was a `conflicts: bool` on each entry, which is state
146 /// that can disagree with the times beside it. Two sources for one fact is
147 /// how a screen starts rendering a conflict badge on a thing that no longer
148 /// conflicts.
149 #[must_use]
150 pub const fn overlaps(self, other: Self) -> bool {
151 self.at < other.end() && other.at < self.end()
152 }
153 }
154
155 /// A time axis: things placed by when they happen, rather than flowed.
156 ///
157 /// # Why this is a primitive
158 ///
159 /// The argument against naming a timeline is that a description expressive
160 /// enough to draw one is a component library wearing a description's name. It
161 /// does not hold here, and being precise about why matters, because the
162 /// reasoning applies to real cases.
163 ///
164 /// What a timeline needs that a [`List`](Region::Pane) does not is **one**
165 /// thing: placement. Where a thing sits is a fact about the thing, the way a
166 /// row's primary text is, and it is not derivable from order. Everything else a
167 /// day view draws -- the labels, the gridlines, the item bodies, the tones --
168 /// is furniture this vocabulary already names. Measured against goingson's
169 /// `day-planning-render.js`, the only members it needed and could not get were
170 /// `at` and `minutes`.
171 ///
172 /// So the timeline was never a component library's worth of vocabulary. It was
173 /// two integers, and the refusal was priced as though it were the whole widget.
174 /// The test that matters is not "does this shape look complicated" but "how
175 /// many members does it actually add, and are they facts or presentation".
176 /// Slot heights, gridline colour, how overlaps stack and which hour scrolls
177 /// into view on open are all presentation and all stay the renderer's, which is
178 /// why they are absent here.
179 ///
180 /// # What it does not carry
181 ///
182 /// No pixel measure, no scroll offset, no drag affordance. A renderer draws the
183 /// span at whatever density its host uses; `makeover-geometry` owns that the
184 /// way it owns everything else measured in pixels.
185 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186 pub struct Track {
187 /// The window the axis covers.
188 pub span: Span,
189 /// The granularity a thing can be placed on, in minutes.
190 ///
191 /// goingson's day view is 15, giving 96 slots across a day. A renderer uses
192 /// it to decide where gridlines fall and what a drop lands on; it does not
193 /// constrain [`Placement`], because data arriving from a calendar does not
194 /// respect anyone's grid.
195 pub slot: u16,
196 /// How often the axis labels itself, in its own unit.
197 ///
198 /// 60 gives an hourly ruler over a 15-minute grid, which is the common
199 /// shape and the reason this is separate from `slot`. Zero means an
200 /// unlabelled axis.
201 pub tick: u16,
202 /// What `span`, `slot`, `tick` and every [`Placement`] on it count.
203 ///
204 /// The one field here a renderer cannot derive, and the reason it exists:
205 /// [`fraction`](Self::fraction) is unit-agnostic, so a day-granularity
206 /// track produced correct geometry under an hours-and-minutes ruler until
207 /// this was added. Geometry never needed it; a label always did.
208 pub unit: Unit,
209 }
210
211 impl Track {
212 /// An ordinary day: midnight to midnight, quarter-hour slots, hourly ticks.
213 pub const DAY: Self = Self {
214 span: Span::DAY,
215 slot: 15,
216 tick: 60,
217 unit: Unit::Minutes,
218 };
219
220 /// A track over `span`, with the day's usual granularity.
221 #[must_use]
222 pub const fn over(span: Span) -> Self {
223 Self {
224 span,
225 slot: 15,
226 tick: 60,
227 unit: Unit::Minutes,
228 }
229 }
230
231 /// A strip of whole days: one slot a day, a label a week.
232 ///
233 /// The shape a stretch of leave or a sprint is drawn on. Not a calendar --
234 /// it does not wrap into weeks, and the crate header says why that
235 /// distinction is the whole of what a month grid still has over this.
236 #[must_use]
237 pub const fn days(span: Span) -> Self {
238 Self {
239 span,
240 slot: 1,
241 tick: 7,
242 unit: Unit::Days,
243 }
244 }
245
246 /// How many slots the axis holds.
247 ///
248 /// Rounded up, so a span that does not divide evenly by `slot` still has a
249 /// slot covering its tail rather than dropping it. Never zero: `slot` of 0
250 /// reads as one slot spanning the whole axis rather than a division by
251 /// zero, since a renderer asking this question has already committed to
252 /// drawing something.
253 #[must_use]
254 pub const fn slots(self) -> u16 {
255 if self.slot == 0 {
256 1
257 } else {
258 self.span.length().div_ceil(self.slot)
259 }
260 }
261
262 /// Where a placement sits on the axis, as a fraction from 0.0 to 1.0.
263 ///
264 /// The one calculation every renderer would otherwise write itself, and the
265 /// place the three would drift apart. Clamped, so a placement outside the
266 /// span draws at the edge rather than off it -- an event running past
267 /// midnight is a real thing and truncating it is better than either
268 /// panicking or drawing it somewhere impossible.
269 #[must_use]
270 pub fn fraction(self, minute: u16) -> f32 {
271 let span = f32::from(self.span.length());
272 let offset = f32::from(minute.saturating_sub(self.span.from()));
273 (offset / span).clamp(0.0, 1.0)
274 }
275 }
276
277 impl Default for Track {
278 fn default() -> Self {
279 Self::DAY
280 }
281 }
282