gitlab-org--gitlab-foss/lib/gitlab/relative_positioning/mover.rb

155 lines
4.8 KiB
Ruby

# 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