diff options
Diffstat (limited to 'lib/gitlab/relative_positioning/mover.rb')
-rw-r--r-- | lib/gitlab/relative_positioning/mover.rb | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/lib/gitlab/relative_positioning/mover.rb b/lib/gitlab/relative_positioning/mover.rb new file mode 100644 index 00000000000..9d891bfbe3b --- /dev/null +++ b/lib/gitlab/relative_positioning/mover.rb @@ -0,0 +1,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 |