Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/relative_positioning/mover.rb')
-rw-r--r--lib/gitlab/relative_positioning/mover.rb155
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