-
Notifications
You must be signed in to change notification settings - Fork 0
/
dimension.cr
456 lines (374 loc) · 11.1 KB
/
dimension.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# Raised when a dimension is asked to exceed its maximum number
# of states.
class DimensionOverflowException < Exception
end
# Raised when a dimension is asked to shrink below the minimum
# number of states.
class DimensionUnderflowException < Exception
end
# A time-ordered snapshot of `Dimension`.
#
# Allows clients to implement an undo/redo system independent
# of `Dimension`.
#
# Also allows clients to peek into `Dimension` at discrete time
# steps for change-awareness.
class DimensionInstant(SubInstant)
# Returns the timestamp when this instant was captured.
getter timestamp : Int64
# Holds the subordinate editor instants when this instant
# was captured.
getter states : Array(SubInstant)
# Returns which editor was selected when this instant
# was captured.
getter selected : Int32
def initialize(@timestamp, @states, @selected)
end
end
# Generic control logic for a dimension (e.g. row, column) of
# `State`s with one currently `selected` `State`.
abstract class DimensionState(State)
def initialize
@states = [] of State
@selected = first_index
min_size.times { append }
end
# Builds and returns a new, *index*-th subordinate `State`.
#
# Must be implemented by the subclass dimension to specify
# the desired (and optionally index-dependent) kind of new,
# subordinate state.
abstract def new_substate_for(index : Int) : State
# Captures and returns an instant of this dimension.
#
# Useful for undo/redo.
abstract def capture : DimensionInstant
# Returns whether there are currently no states in this dimension.
def empty?
first_index == size
end
# Returns the *total* amount of states in this dimension, *including*
# those before and after `first_index` and `last_index`.
def size
@states.size
end
# Returns the amount of characters taken by the separator
# between states (if any).
def sepsize
0
end
# Specifies the minimum amount of states in this dimension.
#
# When created, this dimension will be filled with the
# specified number of states.
#
# When this dimension has exactly `min_size` subordinate
# states, no further deletions are going to be allowed.
def min_size
0
end
# Specifies the maximum amount of states in this dimension.
#
# Beyond this number, no further state additions are going
# to be allowed.
def max_size
size + 1
end
# Returns the index of the first state.
def first_index
0
end
# Returns the index of the last state.
def last_index
size - 1
end
# Clamps *index* in the bounds of this dimension.
def clamp(index : Int)
index.clamp(first_index..last_index)
end
# Returns the index of the previous state.
def prev_index
@selected - 1
end
# Returns the index of the next state.
def next_index
@selected + 1
end
# Returns the *n*-th state, or raises.
def nth(n : Int)
@states[n]
end
# Returns the selected state.
def selected
nth(@selected)
end
# Returns whether the cursor is at the beginning of the selected state.
def cursor_at_start?
return true if empty?
selected.at_start_index?
end
# Returns whether the cursor is at the end of the selected state.
def cursor_at_end?
return true if empty?
selected.at_end_index?
end
# Returns whether the first state is selected.
def first_selected?
@selected == first_index
end
# Returns whether the last state is selected.
def last_selected?
@selected == last_index
end
# Invoked after motion left happens from/to *state*.
def after_moved_left(state : State)
end
# Invoked after motion right happens from/to *state*.
def after_moved_right(state : State)
end
# Selects *index*-th state. Clamps *index* to the bounds of
# the states in this dimension.
#
# Does nothing if there are no states.
def to_nth(index : Int)
@selected, prev = clamp(index), clamp(@selected)
return if empty?
if @selected < prev # Moving left...
after_moved_left(nth(prev))
after_moved_right(selected)
elsif prev < @selected # Moving right...
after_moved_right(nth(prev))
after_moved_left(selected)
end
end
# Adds the given *state* before *index*. Returns *state*.
#
# Raises `DimensionOverflowException` if it was not appended
# due to size restrictions.
def append(index : Int, state : State) : State
raise DimensionOverflowException.new if size == max_size
@states.insert(index, state)
state
end
# Appends a subordinate state (see `new_substate_for`) at
# *index*. Returns the appended state.
#
# Raises `DimensionOverflowException` if the state cannot be
# appended due to size restrictions.
def append(index : Int)
raise DimensionOverflowException.new if size == max_size
append(index, new_substate_for(index))
end
# Appends a subordinate state *state* at *index*. Returns the
# appended state
def append(state : State)
append(last_index + 1, state)
end
# Appends a subordinate state (see `new_substate_for`) at
# the end.
def append
append(last_index + 1)
end
# Selects the previous state.
#
# If *circular* is false, does nothing if the selected state
# is the first.
#
# If *circular* is true, goes to the last state if the selected
# state is the first.
def to_prev(circular = false)
if first_selected?
to_last if circular
return
end
to_nth(prev_index)
end
# Selects the next state.
#
# If *circular* is false, does nothing if the selected state
# is the last.
#
# If *circular* is true, goes to the first state if the selected
# state is the last.
def to_next(circular = false)
if last_selected?
to_first if circular
return
end
to_nth(next_index)
end
# Selects the first state.
#
# Does nothing if there are no states.
def to_first
return if empty?
to_nth(first_index)
after_moved_left(selected)
end
# Selects the last state.
#
# Does nothing if there are no states.
def to_last
return if empty?
to_nth(last_index)
after_moved_right(selected)
end
# **Destructive**: clears and drops the *index*-th state.
#
# If the selected state was dropped, the previous state
# is selected (if any; otherwise, the next available state
# is selected).
#
# Raises `DimensionUnderflowException` if dropping will shrink
# this dimension below the minimum size.
def drop(index : Int)
raise DimensionUnderflowException.new if size == min_size
@states[index].clear
@states.delete_at(index)
to_prev
end
# **Destructive**: clears and drops the selected state.
def drop
drop(@selected)
end
# **Destructive**: wipes out the content of all subordinate
# states, and forgets about them.
def clear
@states.each &.clear
@selected = 0
return if size <= min_size
@states.clear_from(min_size + 1)
end
# Splits the selected state in two at the cursor.
#
# If *backwards* is true, will then select the previous state.
# Otherwise, the selected state will remain unchanged.
#
# Adds a new empty state if cursor is at either end of the
# selected state.
#
# Adds a new empty state if there are no states regardless
# of *backwards*.
def split(backwards : Bool)
if empty?
append
return
end
appended = append(@selected + 1)
splitdist(selected, appended)
return if backwards
to_next
end
# Distributes the content of *left* between *left* and *right*
# according to the rules of this dimension.
#
# *left* comes before *right* but the definition of "before"
# depends on the subclass.
def splitdist(left : State, right : State)
end
# Merges the current state with the previous or next state,
# depending on whether *forward* is true or false.
#
# Does nothing if there are no states.
def merge(forward : Bool)
return if empty?
return if !forward && first_selected?
return if forward && last_selected?
# Merge forward = Merge back from the next field.
to_next if forward
merge!
end
# Merges the current state into the state behind.
#
# The default implementation has nothing to do with actually
# *merging*: it simply drops the current state.
#
# However, implementors may choose to do content-defined
# merging as appropriate.
#
# If you are a user, consider `merge(forward : Bool)` as it
# is much more user-friendly. This method is allowed to raise
# e.g. when there are no states, or when trying to
# merge behind the first state.
def merge!
drop
end
end
# Generic view code for a dimension (e.g. row, column) of
# subordinate `View`s with one currently active view.
#
# Designed to work together with `DimensionState` only. Updates
# the displayed views from `DimensionState`.
#
# Activity of `DimensionView` specifies whether the active view
# should be active *on the next update*.
#
# * `Instant` is the instant type of the displayed `DimensionState`.
#
# * `SubInstant` is the instant type of the subordinate state
# of `DimensionState`.
abstract class DimensionView(View, Instant, SubInstant)
include IView
property position = SF.vector2f(0, 0)
def initialize
@views = [] of View
@selected = 0
end
# Returns the subordinate `View` that is currently selected.
def selected
@views[@selected]
end
# Calculates and returns the full size of this dimension view.
abstract def size : SF::Vector2f
# Builds and returns a new, *index*-th subordinate `View`.
#
# Must be implemented by the subclass dimension to specify
# the desired (and optionally index-dependent) kind of new,
# subordinate view.
abstract def new_subview_for(index : Int) : View
# Updates the positions of a consecutive pair of subordinate
# views, *l* and *r*.
abstract def arrange_cons_pair(left : View, right : View)
# Specifies the position of the first view in this dimension.
def origin
position
end
# Returns a new, *index*-th subordinate view updated according
# to the given *instant*.
#
# Positions the new subordinate view at `origin`.
def new_subview_from(index : Int, instant : SubInstant)
subview = new_subview_for(index)
subview.active = active? && index == @selected
subview.position = origin
subview.update(instant)
subview
end
# Updates this dimension view from the given state *instant*.
def update(instant : Instant)
@views.clear
states = instant.states
return if states.empty?
@selected = instant.selected
views = [] of View
states.each_with_index do |state, index|
views << new_subview_from(index, state)
end
views.each_cons_pair { |l, r| arrange_cons_pair(l, r) }
# To do arrange() subviews must be updated(). However, after
# arrange() subviews' positions have changed, and on such a
# change they want another update().
#
# This leads to us having to call update() twice, which
# shouldn't be that expensive but still looks quite smelly.
views.zip(states) { |view, state| view.update(state) }
@views = views
end
# Updates this dimension view from the given *state*.
def update(state)
update(state.capture)
end
def draw(target, states)
@views.each &.draw(target, states)
end
end