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
|
# frozen_string_literal: true
module Gitlab
module RelativePositioning
class Mover
attr_reader :range, :start_position
def initialize(start, range)
@range = range
@start_position = start
end
def move_to_end(object)
focus = context(object, ignoring: object)
max_pos = focus.max_relative_position
move_to_range_end(focus, max_pos)
end
def move_to_start(object)
focus = context(object, ignoring: object)
min_pos = focus.min_relative_position
move_to_range_start(focus, min_pos)
end
def move(object, first, last)
raise ArgumentError, 'object is required' unless object
lhs = context(first, ignoring: object)
rhs = context(last, ignoring: object)
focus = context(object)
range = RelativePositioning.range(lhs, rhs)
if range.cover?(focus)
# Moving a object already within a range is a no-op
elsif range.open_on_left?
move_to_range_start(focus, range.rhs.relative_position)
elsif range.open_on_right?
move_to_range_end(focus, range.lhs.relative_position)
else
pos_left, pos_right = create_space_between(range)
desired_position = position_between(pos_left, pos_right)
focus.place_at_position(desired_position, range.lhs)
end
end
def context(object, ignoring: nil)
return unless object
ItemContext.new(object, range, ignoring: ignoring)
end
private
def gap_too_small?(pos_a, pos_b)
return false unless pos_a && pos_b
(pos_a - pos_b).abs < MIN_GAP
end
def move_to_range_end(context, max_pos)
range_end = range.last + 1
new_pos = if max_pos.nil?
start_position
elsif gap_too_small?(max_pos, range_end)
max = context.max_sibling
max.ignoring = context.object
max.shift_left
position_between(max.relative_position, range_end)
else
position_between(max_pos, range_end)
end
context.object.relative_position = new_pos
end
def move_to_range_start(context, min_pos)
range_end = range.first - 1
new_pos = if min_pos.nil?
start_position
elsif gap_too_small?(min_pos, range_end)
sib = context.min_sibling
sib.ignoring = context.object
sib.shift_right
position_between(sib.relative_position, range_end)
else
position_between(min_pos, range_end)
end
context.object.relative_position = new_pos
end
def create_space_between(range)
pos_left = range.lhs&.relative_position
pos_right = range.rhs&.relative_position
return [pos_left, pos_right] unless gap_too_small?(pos_left, pos_right)
gap = range.rhs.create_space_left
[pos_left - gap.delta, pos_right]
rescue NoSpaceLeft
gap = range.lhs.create_space_right
[pos_left, pos_right + gap.delta]
end
# This method takes two integer values (positions) and
# calculates the position between them. The range is huge as
# the maximum integer value is 2147483647.
#
# We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
#
# Then we handle one of three cases:
# - If the gap is too small, we raise NoSpaceLeft
# - If the gap is larger than MAX_GAP, we place the new position at most
# IDEAL_DISTANCE from the edge of the gap.
# - otherwise we place the new position at the midpoint.
#
# The new position will always satisfy: pos_before <= midpoint <= pos_after
#
# As a precondition, the gap between pos_before and pos_after MUST be >= 2.
# If the gap is too small, NoSpaceLeft is raised.
#
# @raises NoSpaceLeft
def position_between(pos_before, pos_after)
pos_before ||= range.first
pos_after ||= range.last
pos_before, pos_after = [pos_before, pos_after].sort
gap_width = pos_after - pos_before
if gap_too_small?(pos_before, pos_after)
raise NoSpaceLeft
elsif gap_width > MAX_GAP
if pos_before <= range.first
pos_after - IDEAL_DISTANCE
elsif pos_after >= range.last
pos_before + IDEAL_DISTANCE
else
midpoint(pos_before, pos_after)
end
else
midpoint(pos_before, pos_after)
end
end
def midpoint(lower_bound, upper_bound)
((lower_bound + upper_bound) / 2.0).ceil.clamp(lower_bound, upper_bound - 1)
end
end
end
end
|