require 'whirled_peas/utils/formatted_string' require_relative 'container_coords' require_relative 'painter' module WhirledPeas module Graphics # Abstract Painter for containers. Containers (as the name implies) contain other child # elements and must delegate painting of the children to the children themselves. class ContainerPainter < Painter PADDING = ' ' def initialize(name, settings) super @children = [] end # Paint the common attributes of containers (e.g. border and background color). Any # class that inherits from this one should call `super` at the start of its #paint # method, before painting its children. def paint(canvas, left, top, &block) return unless canvas.writable? return unless needs_printing? canvas_coords = coords(left, top) # Paint the border, background color, and scrollbar starting from the top left # border position, moving down row by row until we reach the bottom border # position stroke_left = canvas_coords.border_left stroke_top = canvas_coords.border_top # All strokes will have the same formatting options formatting = [*settings.border.color, *settings.bg_color] # Paint the top border if the settings call for it if settings.border.top? canvas.stroke(stroke_left, stroke_top, top_border_stroke(canvas_coords), formatting, &block) stroke_top += 1 end # Precalculate the middle border container grids with more than 1 row middle_border = dimensions.num_rows > 1 ? middle_border_stroke(canvas_coords) : '' # Paint each grid row by row dimensions.num_rows.times do |row_num| # In a grid with N rows, we will need to paint N - 1 inner horizontal borders. # This code treats the inner horizontal border as the top of each row except for # the first one. if row_num > 0 && settings.border.inner_horiz? canvas.stroke(stroke_left, stroke_top, middle_border, formatting, &block) stroke_top += 1 end # Paint the interior of each row (horizontal borders, veritical scroll bar and # background color for the padding and content area) canvas_coords.inner_grid_height.times do |row_within_cell| canvas.stroke(stroke_left, stroke_top, content_line_stroke(canvas_coords, row_within_cell), formatting, &block) stroke_top += 1 end # Paint the horizontal scroll bar is the settings call for it if settings.scrollbar.horiz? canvas.stroke(stroke_left, stroke_top, bottom_scroll_stroke(canvas_coords), formatting, &block) stroke_top += 1 end end # Paint the bottom border if the settings call for it if settings.border.bottom? canvas.stroke(stroke_left, stroke_top, bottom_border_stroke(canvas_coords), formatting, &block) stroke_top += 1 end end # Tightly manage access to the children (rather than simply exposing the underlying # array). This allows subclasses to easily modify behavior based on that element's # specific settings. def add_child(child) children << child end def num_children children.length end def children? num_children > 0 end def each_child(&block) children.each(&block) end private attr_reader :children # Determine if there is anything to print for the container (this does not accont for # children, just the border, scrollbar, and background color) def needs_printing? return true if settings.bg_color return true if settings.border.outer? return true if dimensions.num_cols > 1 && settings.border.inner_vert? return true if dimensions.num_rows > 1 && settings.border.inner_horiz? settings.scrollbar.horiz? || settings.scrollbar.vert? end # Return an object that allows easy access to important coordinates within the container, # e.g. the left position where the left border is printed def coords(left, top) ContainerCoords.new(dimensions, settings, left, top) end # @return [Array] a two-item array, the first being the amount of horizontal # spacing to paint *before the first* child and the second being the amount of spacing # to paint *between each* child def horiz_justify_offset(containing_width) if settings.align_center? [(dimensions.content_width - containing_width) / 2, 0] elsif settings.align_right? [dimensions.content_width - containing_width, 0] elsif settings.align_between? return [0, 0] if num_children == 1 [0, (dimensions.content_width - containing_width) / (num_children - 1)] elsif settings.align_around? full_spacing = (dimensions.content_width - containing_width) / num_children [full_spacing / 2, full_spacing] elsif settings.align_evenly? spacing = (dimensions.content_width - containing_width) / (num_children + 1) [spacing, spacing] else [0, 0] end end # @return [Array] a two-item array, the first being the amount of vertical # spacing to paint *above the first* child and the second being the amount of spacing # to paint *between each* child def vert_justify_offset(containing_height) if settings.valign_middle? [(dimensions.content_height - containing_height) / 2, 0] elsif settings.valign_bottom? [dimensions.content_height - containing_height, 0] elsif settings.valign_between? return [0, 0] if num_children == 1 [0, (dimensions.content_height - containing_height) / (num_children - 1)] elsif settings.valign_around? full_spacing = (dimensions.content_height - containing_height) / num_children [full_spacing / 2, full_spacing] elsif settings.valign_evenly? spacing = (dimensions.content_height - containing_height) / (num_children + 1) [spacing, spacing] else [0, 0] end end # Return a stroke for one line of the container # # @param left_border [String] the character to print as the first character if there # is a left border # @param junc_border [String] the character to print as the junction between two grid # columns if there is an inner vertical border # @param right_border [String] the character to print as the last character if there # is a right border # @block [String] the block should yield a string that represents the interior # (including padding) of a grid cell def line_stroke(left_border, junc_border, right_border, &block) stroke = '' stroke += left_border if settings.border.left? dimensions.num_cols.times do |col_num| stroke += junc_border if col_num > 0 && settings.border.inner_vert? stroke += yield end stroke += right_border if settings.border.right? stroke end # Return the stroke for the top border def top_border_stroke(canvas_coords) line_stroke( settings.border.style.top_left, settings.border.style.top_junc, settings.border.style.top_right ) do settings.border.style.top_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0)) end end # Return the stroke for an inner horizontal border def middle_border_stroke(canvas_coords) line_stroke( settings.border.style.left_junc, settings.border.style.cross_junc, settings.border.style.right_junc ) do settings.border.style.middle_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0)) end end # Return the stroke for the bottom border def bottom_border_stroke(canvas_coords) line_stroke( settings.border.style.bottom_left, settings.border.style.bottom_junc, settings.border.style.bottom_right ) do settings.border.style.bottom_horiz * (canvas_coords.inner_grid_width + (settings.scrollbar.vert? ? 1 : 0)) end end # Return the stroke for a grid row between any borders def content_line_stroke(canvas_coords, row_within_cell) line_stroke( settings.border.style.left_vert, settings.border.style.middle_vert, settings.border.style.right_vert, ) do if settings.scrollbar.vert? if dimensions.children_height <= canvas_coords.grid_height || children.first.settings.position.top > 0 scrollbar_char = GUTTER else scrollbar_char = vert_scroll_char( dimensions.children_height + dimensions.padding_height, canvas_coords.inner_grid_height, -children.first.settings.position.top, row_within_cell ) end PADDING * canvas_coords.inner_grid_width + scrollbar_char else PADDING * canvas_coords.inner_grid_width end end end # Return the stroke for the horizontal scroll bar def bottom_scroll_stroke(canvas_coords) line_stroke( settings.border.style.left_vert, settings.border.style.middle_vert, settings.border.style.right_vert, ) do canvas_coords.inner_grid_width.times.map do |col_within_cell| horiz_scroll_char( dimensions.children_width + dimensions.padding_width, canvas_coords.inner_grid_width, -children.first.settings.position.left, col_within_cell ) end.join end end # Contants to paint scrollbars GUTTER = ' ' HORIZONTAL = %w[▗ ▄ ▖] VERTICAL = %w[ ▗ ▐ ▝ ] # Determine the character to paint the horizontal scroll bar with for the given column # # @see #scroll_char for more details def horiz_scroll_char(col_count, viewable_col_count, first_visible_col, curr_col) scroll_char(col_count, viewable_col_count, first_visible_col, curr_col, HORIZONTAL) end # Determine the character to paint the vertical scroll bar with for the given row # # @see #scroll_char for more details def vert_scroll_char(row_count, viewable_row_count, first_visible_row, curr_row) scroll_char(row_count, viewable_row_count, first_visible_row, curr_row, VERTICAL) end private # Determine which character to paint a for a scrollbar # # @param total_count [Integer] total number of rows/columns in the content # @param viewable_count [Integer] number of rows/columns visible in the viewport # @param first_visible [Integer] zero-based index of the first row/column that is visible # in the viewport # @param curr [Integer] zero-based index of the row/column (relative to the first visible # row/column) that the painted character is being requested for # @param chars [Array] an array with three 1-character strings, the frist is the # "second half" scrollbar character, the second is the "full" scrollbar character, and # the third is the "first half" scrollbar character. def scroll_char(total_count, viewable_count, first_visible, curr, chars) return GUTTER unless total_count > 0 && viewable_count > 0 # The scroll handle has the exact same relative size and position in the scroll gutter # that the viewable content has in the total content area. For example, a content area # that is 50 columns wide with a view port that is 20 columns wide might look like # # +---------1-----****2*********3******---4---------+ # | * * | # | hidden * viewable * hidden | # | * * | # +---------1-----****2*********3******---4---------+ # # The scoll gutter, would look like # # |......********.....| # # Scrolling all the way to the right results in # # +---------1---------2---------3*********4*********+ # | * * # | hidden * viewable * # | * * # +---------1---------2---------3*********4*********+ # |...........********| # # Returning to the first example, we can match up the arguments to this method to the # diagram # # total_count = 50 # |<----------------------------------------------->| # | | # | veiwable_count = 20 | # | |<----------------->| | # ↓ ↓ ↓ ↓ # +---------1-----****2*********3******---4---------+ # | * * | # | hidden * viewable * hidden | # | * * | # +---------1-----****2*********3******---4---------+ # |......****?***.....| # ↑ ↑ # first_visible = 16 | # curr = 11 # The first task of determining how much of the handle is visible in a row/column is to # calculate the range (as a precentage of the total) of viewable items viewable_start = first_visible.to_f / total_count viewable_end = (first_visible + viewable_count).to_f / total_count # Always use the same length for the scroll bar so it does not give an inchworm effect # as it scrolls along. # # Also, double the value now to get granularity for half width # scrollbar characters. scrollbar_length = ((2 * viewable_count ** 2).to_f / total_count).ceil scrollbar_start = ((2 * first_visible * viewable_count).to_f / total_count).floor first_half = (scrollbar_start...scrollbar_start + scrollbar_length).include?(2 * curr) second_half = (scrollbar_start...scrollbar_start + scrollbar_length).include?(2 * curr + 1) if first_half && second_half chars[1] elsif first_half chars[2] elsif second_half chars[0] else GUTTER end end end private_constant :ContainerPainter end end