whirled_peas 0.4.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -0
- data/CHANGELOG.md +37 -0
- data/README.md +529 -156
- data/Rakefile +9 -3
- data/bin/reset_cursor +11 -0
- data/bin/screen_test +68 -0
- data/examples/intro.rb +52 -0
- data/examples/scrolling.rb +54 -0
- data/lib/whirled_peas.rb +6 -12
- data/lib/whirled_peas/animator.rb +5 -0
- data/lib/whirled_peas/animator/debug_consumer.rb +17 -0
- data/lib/whirled_peas/animator/easing.rb +72 -0
- data/lib/whirled_peas/animator/frame.rb +5 -0
- data/lib/whirled_peas/animator/frameset.rb +33 -0
- data/lib/whirled_peas/animator/producer.rb +35 -0
- data/lib/whirled_peas/animator/renderer_consumer.rb +31 -0
- data/lib/whirled_peas/command.rb +5 -0
- data/lib/whirled_peas/command/base.rb +86 -0
- data/lib/whirled_peas/command/config_command.rb +44 -0
- data/lib/whirled_peas/command/debug.rb +21 -0
- data/lib/whirled_peas/command/fonts.rb +22 -0
- data/lib/whirled_peas/command/frame_command.rb +34 -0
- data/lib/whirled_peas/command/frames.rb +24 -0
- data/lib/whirled_peas/command/help.rb +38 -0
- data/lib/whirled_peas/command/play.rb +108 -0
- data/lib/whirled_peas/command/record.rb +57 -0
- data/lib/whirled_peas/command/still.rb +29 -0
- data/lib/whirled_peas/command_line.rb +23 -228
- data/lib/whirled_peas/config.rb +56 -6
- data/lib/whirled_peas/device.rb +5 -0
- data/lib/whirled_peas/device/null_device.rb +8 -0
- data/lib/whirled_peas/device/output_file.rb +19 -0
- data/lib/whirled_peas/device/screen.rb +26 -0
- data/lib/whirled_peas/errors.rb +2 -0
- data/lib/whirled_peas/graphics.rb +19 -0
- data/lib/whirled_peas/graphics/box_painter.rb +101 -0
- data/lib/whirled_peas/graphics/canvas.rb +118 -0
- data/lib/whirled_peas/graphics/composer.rb +80 -0
- data/lib/whirled_peas/graphics/container_coords.rb +72 -0
- data/lib/whirled_peas/graphics/container_dimensions.rb +93 -0
- data/lib/whirled_peas/graphics/container_painter.rb +363 -0
- data/lib/whirled_peas/graphics/debugger.rb +52 -0
- data/lib/whirled_peas/graphics/grid_painter.rb +69 -0
- data/lib/whirled_peas/graphics/mock_screen.rb +26 -0
- data/lib/whirled_peas/graphics/painter.rb +33 -0
- data/lib/whirled_peas/graphics/renderer.rb +32 -0
- data/lib/whirled_peas/graphics/text_dimensions.rb +15 -0
- data/lib/whirled_peas/graphics/text_painter.rb +40 -0
- data/lib/whirled_peas/settings.rb +5 -0
- data/lib/whirled_peas/settings/alignment.rb +24 -0
- data/lib/whirled_peas/settings/bg_color.rb +24 -0
- data/lib/whirled_peas/settings/border.rb +101 -0
- data/lib/whirled_peas/settings/box_settings.rb +8 -0
- data/lib/whirled_peas/settings/color.rb +68 -0
- data/lib/whirled_peas/settings/container_settings.rb +223 -0
- data/lib/whirled_peas/settings/debugger.rb +96 -0
- data/lib/whirled_peas/settings/display_flow.rb +27 -0
- data/lib/whirled_peas/settings/element_settings.rb +61 -0
- data/lib/whirled_peas/settings/grid_settings.rb +19 -0
- data/lib/whirled_peas/settings/margin.rb +8 -0
- data/lib/whirled_peas/settings/padding.rb +8 -0
- data/lib/whirled_peas/settings/position.rb +15 -0
- data/lib/whirled_peas/settings/scrollbar.rb +15 -0
- data/lib/whirled_peas/settings/sizing.rb +19 -0
- data/lib/whirled_peas/settings/spacing.rb +58 -0
- data/lib/whirled_peas/settings/text_color.rb +21 -0
- data/lib/whirled_peas/settings/text_settings.rb +15 -0
- data/lib/whirled_peas/settings/vert_alignment.rb +24 -0
- data/lib/whirled_peas/utils/ansi.rb +19 -56
- data/lib/whirled_peas/utils/file_handler.rb +57 -0
- data/lib/whirled_peas/utils/formatted_string.rb +64 -0
- data/lib/whirled_peas/utils/title_font.rb +1 -1
- data/lib/whirled_peas/version.rb +1 -1
- data/screen_test/elements/box.frame +1 -0
- data/screen_test/elements/box.rb +20 -0
- data/screen_test/elements/grid.frame +1 -0
- data/screen_test/elements/grid.rb +13 -0
- data/screen_test/elements/screen_overflow_x.frame +1 -0
- data/screen_test/elements/screen_overflow_x.rb +9 -0
- data/screen_test/elements/screen_overflow_y.frame +1 -0
- data/screen_test/elements/screen_overflow_y.rb +9 -0
- data/screen_test/elements/text.frame +1 -0
- data/screen_test/elements/text.rb +9 -0
- data/screen_test/elements/text_multiline.frame +1 -0
- data/screen_test/elements/text_multiline.rb +9 -0
- data/screen_test/settings/align/box_around.frame +1 -0
- data/screen_test/settings/align/box_around.rb +16 -0
- data/screen_test/settings/align/box_between.frame +1 -0
- data/screen_test/settings/align/box_between.rb +16 -0
- data/screen_test/settings/align/box_center.frame +1 -0
- data/screen_test/settings/align/box_center.rb +21 -0
- data/screen_test/settings/align/box_default.frame +1 -0
- data/screen_test/settings/align/box_default.rb +20 -0
- data/screen_test/settings/align/box_evenly.frame +1 -0
- data/screen_test/settings/align/box_evenly.rb +16 -0
- data/screen_test/settings/align/box_left.frame +1 -0
- data/screen_test/settings/align/box_left.rb +21 -0
- data/screen_test/settings/align/box_right.frame +1 -0
- data/screen_test/settings/align/box_right.rb +21 -0
- data/screen_test/settings/align/children_center.frame +1 -0
- data/screen_test/settings/align/children_center.rb +15 -0
- data/screen_test/settings/align/children_left.frame +1 -0
- data/screen_test/settings/align/children_left.rb +15 -0
- data/screen_test/settings/align/children_right.frame +1 -0
- data/screen_test/settings/align/children_right.rb +15 -0
- data/screen_test/settings/align/grid_center.frame +1 -0
- data/screen_test/settings/align/grid_center.rb +18 -0
- data/screen_test/settings/align/grid_default.frame +1 -0
- data/screen_test/settings/align/grid_default.rb +17 -0
- data/screen_test/settings/align/grid_left.frame +1 -0
- data/screen_test/settings/align/grid_left.rb +18 -0
- data/screen_test/settings/align/grid_right.frame +1 -0
- data/screen_test/settings/align/grid_right.rb +18 -0
- data/screen_test/settings/ansi/bold.frame +1 -0
- data/screen_test/settings/ansi/bold.rb +14 -0
- data/screen_test/settings/ansi/color.frame +1 -0
- data/screen_test/settings/ansi/color.rb +37 -0
- data/screen_test/settings/ansi/underline.frame +1 -0
- data/screen_test/settings/ansi/underline.rb +14 -0
- data/screen_test/settings/border.frame +1 -0
- data/screen_test/settings/border.rb +13 -0
- data/screen_test/settings/flow/box_b2t.frame +1 -0
- data/screen_test/settings/flow/box_b2t.rb +26 -0
- data/screen_test/settings/flow/box_l2r.frame +1 -0
- data/screen_test/settings/flow/box_l2r.rb +26 -0
- data/screen_test/settings/flow/box_r2l.frame +1 -0
- data/screen_test/settings/flow/box_r2l.rb +26 -0
- data/screen_test/settings/flow/box_t2b.frame +1 -0
- data/screen_test/settings/flow/box_t2b.rb +26 -0
- data/screen_test/settings/flow/grid_b2t.frame +1 -0
- data/screen_test/settings/flow/grid_b2t.rb +14 -0
- data/screen_test/settings/flow/grid_l2r.frame +1 -0
- data/screen_test/settings/flow/grid_l2r.rb +14 -0
- data/screen_test/settings/flow/grid_r2l.frame +1 -0
- data/screen_test/settings/flow/grid_r2l.rb +14 -0
- data/screen_test/settings/flow/grid_t2b.frame +1 -0
- data/screen_test/settings/flow/grid_t2b.rb +14 -0
- data/screen_test/settings/height/box.frame +1 -0
- data/screen_test/settings/height/box.rb +13 -0
- data/screen_test/settings/height/box_border_sizing.frame +1 -0
- data/screen_test/settings/height/box_border_sizing.rb +15 -0
- data/screen_test/settings/height/grid.frame +1 -0
- data/screen_test/settings/height/grid.rb +14 -0
- data/screen_test/settings/height/overflow_box.frame +1 -0
- data/screen_test/settings/height/overflow_box.rb +13 -0
- data/screen_test/settings/height/overflow_box_l2r.frame +1 -0
- data/screen_test/settings/height/overflow_box_l2r.rb +17 -0
- data/screen_test/settings/height/overflow_box_t2b.frame +1 -0
- data/screen_test/settings/height/overflow_box_t2b.rb +16 -0
- data/screen_test/settings/height/overflow_grid.frame +1 -0
- data/screen_test/settings/height/overflow_grid.rb +16 -0
- data/screen_test/settings/margin.frame +1 -0
- data/screen_test/settings/margin.rb +16 -0
- data/screen_test/settings/padding.frame +1 -0
- data/screen_test/settings/padding.rb +13 -0
- data/screen_test/settings/position/box_left.frame +1 -0
- data/screen_test/settings/position/box_left.rb +17 -0
- data/screen_test/settings/position/box_left_negative.frame +1 -0
- data/screen_test/settings/position/box_left_negative.rb +17 -0
- data/screen_test/settings/position/box_top.frame +1 -0
- data/screen_test/settings/position/box_top.rb +17 -0
- data/screen_test/settings/position/box_top_negative.frame +1 -0
- data/screen_test/settings/position/box_top_negative.rb +17 -0
- data/screen_test/settings/position/grid_left.frame +1 -0
- data/screen_test/settings/position/grid_left.rb +18 -0
- data/screen_test/settings/position/grid_left_negative.frame +1 -0
- data/screen_test/settings/position/grid_left_negative.rb +18 -0
- data/screen_test/settings/position/grid_top.frame +1 -0
- data/screen_test/settings/position/grid_top.rb +18 -0
- data/screen_test/settings/position/grid_top_negative.frame +1 -0
- data/screen_test/settings/position/grid_top_negative.rb +18 -0
- data/screen_test/settings/scroll/horiz_box.frame +1 -0
- data/screen_test/settings/scroll/horiz_box.rb +17 -0
- data/screen_test/settings/scroll/horiz_box_align_center.rb +18 -0
- data/screen_test/settings/scroll/horiz_box_align_right.rb +18 -0
- data/screen_test/settings/scroll/vert_box.frame +1 -0
- data/screen_test/settings/scroll/vert_box.rb +20 -0
- data/screen_test/settings/title_font.frame +1 -0
- data/screen_test/settings/title_font.rb +12 -0
- data/screen_test/settings/valign/box_around.frame +1 -0
- data/screen_test/settings/valign/box_around.rb +17 -0
- data/screen_test/settings/valign/box_between.frame +1 -0
- data/screen_test/settings/valign/box_between.rb +17 -0
- data/screen_test/settings/valign/box_bottom.frame +1 -0
- data/screen_test/settings/valign/box_bottom.rb +17 -0
- data/screen_test/settings/valign/box_default.frame +1 -0
- data/screen_test/settings/valign/box_default.rb +16 -0
- data/screen_test/settings/valign/box_evenly.frame +1 -0
- data/screen_test/settings/valign/box_evenly.rb +17 -0
- data/screen_test/settings/valign/box_middle.frame +1 -0
- data/screen_test/settings/valign/box_middle.rb +17 -0
- data/screen_test/settings/valign/box_top.frame +1 -0
- data/screen_test/settings/valign/box_top.rb +17 -0
- data/screen_test/settings/valign/grid_bottom.frame +1 -0
- data/screen_test/settings/valign/grid_bottom.rb +15 -0
- data/screen_test/settings/valign/grid_default.frame +1 -0
- data/screen_test/settings/valign/grid_default.rb +14 -0
- data/screen_test/settings/valign/grid_middle.frame +1 -0
- data/screen_test/settings/valign/grid_middle.rb +15 -0
- data/screen_test/settings/valign/grid_top.frame +1 -0
- data/screen_test/settings/valign/grid_top.rb +15 -0
- data/screen_test/settings/width/box_border_sizing.frame +1 -0
- data/screen_test/settings/width/box_border_sizing.rb +15 -0
- data/screen_test/settings/width/box_content.frame +1 -0
- data/screen_test/settings/width/box_content.rb +15 -0
- data/screen_test/settings/width/box_default.frame +1 -0
- data/screen_test/settings/width/box_default.rb +14 -0
- data/screen_test/settings/width/grid.frame +1 -0
- data/screen_test/settings/width/grid.rb +14 -0
- data/screen_test/settings/width/overflow_align_center.frame +1 -0
- data/screen_test/settings/width/overflow_align_center.rb +14 -0
- data/screen_test/settings/width/overflow_align_right.frame +1 -0
- data/screen_test/settings/width/overflow_align_right.rb +14 -0
- data/screen_test/settings/width/overflow_box.frame +1 -0
- data/screen_test/settings/width/overflow_box.rb +13 -0
- data/screen_test/settings/width/overflow_box_l2r.frame +1 -0
- data/screen_test/settings/width/overflow_box_l2r.rb +16 -0
- data/screen_test/settings/width/overflow_box_t2b.frame +1 -0
- data/screen_test/settings/width/overflow_box_t2b.rb +17 -0
- data/screen_test/settings/width/overflow_grid.frame +1 -0
- data/screen_test/settings/width/overflow_grid.rb +14 -0
- data/tools/whirled_peas/tools/screen_tester.rb +285 -0
- metadata +213 -15
- data/bin/title_fonts +0 -6
- data/lib/whirled_peas/frame.rb +0 -7
- data/lib/whirled_peas/frame/event_loop.rb +0 -91
- data/lib/whirled_peas/frame/print_consumer.rb +0 -33
- data/lib/whirled_peas/frame/producer.rb +0 -61
- data/lib/whirled_peas/template.rb +0 -5
- data/lib/whirled_peas/template/element.rb +0 -230
- data/lib/whirled_peas/template/settings.rb +0 -530
- data/lib/whirled_peas/ui.rb +0 -5
- data/lib/whirled_peas/ui/canvas.rb +0 -68
- data/lib/whirled_peas/ui/painter.rb +0 -287
- data/lib/whirled_peas/ui/screen.rb +0 -63
- data/lib/whirled_peas/utils/color.rb +0 -101
| @@ -0,0 +1,52 @@ | |
| 1 | 
            +
            require 'whirled_peas/settings/debugger'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'container_painter'
         | 
| 4 | 
            +
            require_relative 'text_painter'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module WhirledPeas
         | 
| 7 | 
            +
              module Graphics
         | 
| 8 | 
            +
                class Debugger
         | 
| 9 | 
            +
                  def initialize(painter)
         | 
| 10 | 
            +
                    @painter = painter
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def debug(indent='')
         | 
| 14 | 
            +
                    info = [
         | 
| 15 | 
            +
                      "#{indent}* #{painter.class}(#{painter.name})",
         | 
| 16 | 
            +
                    ]
         | 
| 17 | 
            +
                    info << "#{indent + '  '}- Dimensions(#{dimensions})"
         | 
| 18 | 
            +
                    info << "#{indent + '  '}- Settings"
         | 
| 19 | 
            +
                    info << Settings::Debugger.new(painter.settings).debug(indent + '    ')
         | 
| 20 | 
            +
                    if painter.is_a?(TextPainter)
         | 
| 21 | 
            +
                      content = if painter.content.length > 1
         | 
| 22 | 
            +
                        '<multiline>'
         | 
| 23 | 
            +
                      elsif painter.content.first.length > 12
         | 
| 24 | 
            +
                        "#{painter.content.first[0..9]}...".inspect
         | 
| 25 | 
            +
                      else
         | 
| 26 | 
            +
                        painter.content.first.inspect
         | 
| 27 | 
            +
                      end
         | 
| 28 | 
            +
                      info << "#{indent + '  '}- Content(value=#{content})"
         | 
| 29 | 
            +
                    elsif painter.is_a?(ContainerPainter)
         | 
| 30 | 
            +
                      info << "#{indent + '  '}- Children"
         | 
| 31 | 
            +
                      info += painter.each_child.map { |c| Debugger.new(c).debug(indent + '    ') }
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                    info.join("\n")
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  private
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  attr_reader :painter
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def dimensions
         | 
| 41 | 
            +
                    outer = "#{painter.dimensions.outer_width}x#{painter.dimensions.outer_height}"
         | 
| 42 | 
            +
                    if painter.is_a?(ContainerPainter)
         | 
| 43 | 
            +
                      content = "#{painter.dimensions.content_width}x#{painter.dimensions.content_height}"
         | 
| 44 | 
            +
                      grid = "#{painter.dimensions.num_cols}x#{painter.dimensions.num_rows}"
         | 
| 45 | 
            +
                      "outer=#{outer}, content=#{content}, grid=#{grid}"
         | 
| 46 | 
            +
                    else
         | 
| 47 | 
            +
                      "outer=#{outer}"
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
            end
         | 
| @@ -0,0 +1,69 @@ | |
| 1 | 
            +
            require_relative 'container_dimensions'
         | 
| 2 | 
            +
            require_relative 'container_painter'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module WhirledPeas
         | 
| 5 | 
            +
              module Graphics
         | 
| 6 | 
            +
                class GridPainter < ContainerPainter
         | 
| 7 | 
            +
                  def paint(canvas, left, top, &block)
         | 
| 8 | 
            +
                    super
         | 
| 9 | 
            +
                    return unless canvas.writable?
         | 
| 10 | 
            +
                    each_child.with_index do |child, index|
         | 
| 11 | 
            +
                      col_index, row_index = grid_cell(index)
         | 
| 12 | 
            +
                      left_offset, _ = horiz_justify_offset(child.dimensions.outer_width)
         | 
| 13 | 
            +
                      top_offset, _ = vert_justify_offset(child.dimensions.outer_height)
         | 
| 14 | 
            +
                      child_canvas = canvas.child(
         | 
| 15 | 
            +
                        coords(left, top).content_left(col_index) + left_offset,
         | 
| 16 | 
            +
                        coords(left, top).content_top(row_index) + top_offset,
         | 
| 17 | 
            +
                        [dimensions.content_width, child.dimensions.outer_width].min,
         | 
| 18 | 
            +
                        [dimensions.content_height, child.dimensions.outer_height].min
         | 
| 19 | 
            +
                      )
         | 
| 20 | 
            +
                      child.paint(
         | 
| 21 | 
            +
                        child_canvas,
         | 
| 22 | 
            +
                        coords(left, top).content_left(col_index) + left_offset,
         | 
| 23 | 
            +
                        coords(left, top).content_top(row_index) + top_offset,
         | 
| 24 | 
            +
                        &block
         | 
| 25 | 
            +
                      )
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def dimensions
         | 
| 30 | 
            +
                    @dimensions ||= begin
         | 
| 31 | 
            +
                      if settings.num_cols.nil? || settings.num_cols == 0
         | 
| 32 | 
            +
                        raise SettingsError, "`num_cols` must be set for GridSettings(#{name})"
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
                      num_cols = settings.num_cols
         | 
| 35 | 
            +
                      num_rows = (num_children.to_f / num_cols).ceil
         | 
| 36 | 
            +
                      content_width = 0
         | 
| 37 | 
            +
                      each_child do |child|
         | 
| 38 | 
            +
                        if child.dimensions.outer_width > content_width
         | 
| 39 | 
            +
                          content_width = child.dimensions.outer_width
         | 
| 40 | 
            +
                        end
         | 
| 41 | 
            +
                      end
         | 
| 42 | 
            +
                      content_height = 0
         | 
| 43 | 
            +
                      each_child do |child|
         | 
| 44 | 
            +
                        if child.dimensions.outer_height > content_height
         | 
| 45 | 
            +
                          content_height = child.dimensions.outer_height
         | 
| 46 | 
            +
                        end
         | 
| 47 | 
            +
                      end
         | 
| 48 | 
            +
                      ContainerDimensions.new(
         | 
| 49 | 
            +
                        settings, content_width, content_height, num_cols, num_rows
         | 
| 50 | 
            +
                      )
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  private
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def grid_cell(index)
         | 
| 57 | 
            +
                    if settings.horizontal_flow?
         | 
| 58 | 
            +
                      col_index, row_index = [index % dimensions.num_cols, index / dimensions.num_cols]
         | 
| 59 | 
            +
                      col_index = dimensions.num_cols - col_index - 1 if settings.reverse_flow?
         | 
| 60 | 
            +
                    else
         | 
| 61 | 
            +
                      col_index, row_index = [index / dimensions.num_rows, index % dimensions.num_rows]
         | 
| 62 | 
            +
                      row_index = dimensions.num_rows - row_index - 1 if settings.reverse_flow?
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                    [col_index, row_index]
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
                private_constant :BoxPainter
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
            end
         | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            require_relative 'renderer'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module WhirledPeas
         | 
| 4 | 
            +
              module Graphics
         | 
| 5 | 
            +
                class MockScreen
         | 
| 6 | 
            +
                  def initialize(width:, height:)
         | 
| 7 | 
            +
                    @width = width
         | 
| 8 | 
            +
                    @height = height
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def paint(template)
         | 
| 12 | 
            +
                    Renderer.new(template, width, height).paint { }
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def refresh
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def finalize
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  private
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  attr_reader :width, :height
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            module WhirledPeas
         | 
| 2 | 
            +
              module Graphics
         | 
| 3 | 
            +
                # Abstract base Painter class. Given a canvas and start coordinates (left, top), a painter
         | 
| 4 | 
            +
                # is responsible for generating the "strokes" that display the element.
         | 
| 5 | 
            +
                class Painter
         | 
| 6 | 
            +
                  attr_reader :name, :settings
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(name, settings)
         | 
| 9 | 
            +
                    @settings = settings
         | 
| 10 | 
            +
                    @name = name
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  # Paint the element onto the canvas by yielding strokes to the block. A stroke is composed
         | 
| 14 | 
            +
                  # of a left, top, and chars. E.g.
         | 
| 15 | 
            +
                  #
         | 
| 16 | 
            +
                  #   yield 10, 3, 'Hello World!'
         | 
| 17 | 
            +
                  #
         | 
| 18 | 
            +
                  # paints the string "Hello World!" in the 10th column from the left, 3rd row down.
         | 
| 19 | 
            +
                  def paint(canvas, left, top, &block)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  # Return a dimension object that provider the `outer_width` and `outer_height` of the
         | 
| 23 | 
            +
                  # element being painted.
         | 
| 24 | 
            +
                  def dimensions
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def inspect
         | 
| 28 | 
            +
                    "#{self.class.name.split('::').last}(name=#{name.inspect})"
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
                private_constant :Painter
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            require_relative 'canvas'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module WhirledPeas
         | 
| 4 | 
            +
              module Graphics
         | 
| 5 | 
            +
                class Renderer
         | 
| 6 | 
            +
                  def initialize(template, width, height)
         | 
| 7 | 
            +
                    @template = template
         | 
| 8 | 
            +
                    @width = width
         | 
| 9 | 
            +
                    @height = height
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def paint
         | 
| 13 | 
            +
                    # Modify the template's settings so that it fills the entire screen
         | 
| 14 | 
            +
                    template.settings.width = width
         | 
| 15 | 
            +
                    template.settings.height = height
         | 
| 16 | 
            +
                    template.settings.sizing = :border
         | 
| 17 | 
            +
                    template.settings.set_margin(left: 0, top: 0, right: 0, bottom: 0)
         | 
| 18 | 
            +
                    strokes = [Utils::Ansi.cursor_visible(false), Utils::Ansi.cursor_pos, Utils::Ansi.clear_down]
         | 
| 19 | 
            +
                    template.paint(Canvas.new(0, 0, width, height), 0, 0) do |left, top, fstring|
         | 
| 20 | 
            +
                      next unless fstring.length > 0
         | 
| 21 | 
            +
                      strokes << Utils::Ansi.cursor_pos(left: left, top: top)
         | 
| 22 | 
            +
                      strokes << fstring
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                    strokes.join
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  private
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  attr_reader :template, :width, :height
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
            end
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            module WhirledPeas
         | 
| 2 | 
            +
              module Graphics
         | 
| 3 | 
            +
                class TextDimensions
         | 
| 4 | 
            +
                  attr_reader :outer_width, :outer_height
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(content)
         | 
| 7 | 
            +
                    @outer_width = 0
         | 
| 8 | 
            +
                    content.each do |line|
         | 
| 9 | 
            +
                      @outer_width = line.length if line.length > @outer_width
         | 
| 10 | 
            +
                    end
         | 
| 11 | 
            +
                    @outer_height = content.length
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            require 'whirled_peas/utils/formatted_string'
         | 
| 2 | 
            +
            require 'whirled_peas/utils/title_font'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require_relative 'painter'
         | 
| 5 | 
            +
            require_relative 'text_dimensions'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module WhirledPeas
         | 
| 8 | 
            +
              module Graphics
         | 
| 9 | 
            +
                class TextPainter < Painter
         | 
| 10 | 
            +
                  attr_reader :content
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def paint(canvas, left, top, &block)
         | 
| 13 | 
            +
                    return unless canvas.writable?
         | 
| 14 | 
            +
                    formatting = [*settings.color, *settings.bg_color]
         | 
| 15 | 
            +
                    formatting << Utils::Ansi::BOLD if settings.bold?
         | 
| 16 | 
            +
                    if settings.underline? && settings.title_font.nil?
         | 
| 17 | 
            +
                      formatting << Utils::Ansi::UNDERLINE
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                    content.each.with_index do |line, index|
         | 
| 20 | 
            +
                      canvas.stroke(left, top + index, line, formatting, &block)
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def dimensions
         | 
| 25 | 
            +
                    TextDimensions.new(content)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def content=(content)
         | 
| 29 | 
            +
                    @content = if settings.title_font
         | 
| 30 | 
            +
                      Utils::TitleFont.to_s(
         | 
| 31 | 
            +
                        content, settings.title_font
         | 
| 32 | 
            +
                      ).split("\n")
         | 
| 33 | 
            +
                    else
         | 
| 34 | 
            +
                      content.split("\n")
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
                private_constant :TextPainter
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            module WhirledPeas
         | 
| 2 | 
            +
              module Settings
         | 
| 3 | 
            +
                module Alignment
         | 
| 4 | 
            +
                  LEFT = :left
         | 
| 5 | 
            +
                  CENTER = :center
         | 
| 6 | 
            +
                  RIGHT = :right
         | 
| 7 | 
            +
                  BETWEEN = :between
         | 
| 8 | 
            +
                  AROUND = :around
         | 
| 9 | 
            +
                  EVENLY = :evenly
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  DEFAULT = LEFT
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  VALID = [LEFT, CENTER, RIGHT, BETWEEN, AROUND, EVENLY]
         | 
| 14 | 
            +
                  private_constant :VALID
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def self.validate!(align)
         | 
| 17 | 
            +
                    return if align.nil?
         | 
| 18 | 
            +
                    return align if VALID.include?(align)
         | 
| 19 | 
            +
                    raise ArgumentError, "Unsupported alignment: #{align.inspect}"
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
                private_constant :Alignment
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            require 'whirled_peas/utils/ansi'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'color'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module WhirledPeas
         | 
| 6 | 
            +
              module Settings
         | 
| 7 | 
            +
                class BgColor < Color
         | 
| 8 | 
            +
                  BG_OFFSET = 10
         | 
| 9 | 
            +
                  private_constant :BG_OFFSET
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  BLACK = new(Utils::Ansi::BLACK + BG_OFFSET)
         | 
| 12 | 
            +
                  RED = new(Utils::Ansi::RED + BG_OFFSET)
         | 
| 13 | 
            +
                  GREEN = new(Utils::Ansi::GREEN + BG_OFFSET)
         | 
| 14 | 
            +
                  YELLOW = new(Utils::Ansi::YELLOW + BG_OFFSET)
         | 
| 15 | 
            +
                  BLUE = new(Utils::Ansi::BLUE + BG_OFFSET)
         | 
| 16 | 
            +
                  MAGENTA = new(Utils::Ansi::MAGENTA + BG_OFFSET)
         | 
| 17 | 
            +
                  CYAN = new(Utils::Ansi::CYAN + BG_OFFSET)
         | 
| 18 | 
            +
                  WHITE = new(Utils::Ansi::WHITE + BG_OFFSET)
         | 
| 19 | 
            +
                  GRAY = BLACK.bright
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  DEFAULT = nil  # Use system defined default
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,101 @@ | |
| 1 | 
            +
            require_relative 'text_color'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module WhirledPeas
         | 
| 4 | 
            +
              module Settings
         | 
| 5 | 
            +
                class Border
         | 
| 6 | 
            +
                  Style = Struct.new(
         | 
| 7 | 
            +
                    :top_left, :top_horiz, :top_junc, :top_right,
         | 
| 8 | 
            +
                    :left_vert, :left_junc,
         | 
| 9 | 
            +
                    :middle_vert, :cross_junc, :middle_horiz,
         | 
| 10 | 
            +
                    :right_vert, :right_junc,
         | 
| 11 | 
            +
                    :bottom_left, :bottom_horiz, :bottom_junc, :bottom_right
         | 
| 12 | 
            +
                  )
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  module Styles
         | 
| 15 | 
            +
                    BOLD = Style.new(
         | 
| 16 | 
            +
                      '┏', '━', '┳', '┓',
         | 
| 17 | 
            +
                      '┃', '┣',
         | 
| 18 | 
            +
                      '┃', '╋', '━',
         | 
| 19 | 
            +
                      '┃', '┫',
         | 
| 20 | 
            +
                      '┗', '━', '┻', '┛'
         | 
| 21 | 
            +
                    )
         | 
| 22 | 
            +
                    SOFT = Style.new(
         | 
| 23 | 
            +
                      '╭', '─', '┬', '╮',
         | 
| 24 | 
            +
                      '│', '├',
         | 
| 25 | 
            +
                      '│', '┼', '─',
         | 
| 26 | 
            +
                      '│', '┤',
         | 
| 27 | 
            +
                      '╰', '─', '┴', '╯'
         | 
| 28 | 
            +
                    )
         | 
| 29 | 
            +
                    DOUBLE = Style.new(
         | 
| 30 | 
            +
                      '╔', '═', '╦', '╗',
         | 
| 31 | 
            +
                      '║', '╠',
         | 
| 32 | 
            +
                      '║', '╬', '═',
         | 
| 33 | 
            +
                      '║', '╣',
         | 
| 34 | 
            +
                      '╚', '═', '╩', '╝'
         | 
| 35 | 
            +
                    )
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    DEFAULT = BOLD
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    def self.validate!(style)
         | 
| 40 | 
            +
                      return if style.nil?
         | 
| 41 | 
            +
                      return style if style.is_a?(Border::Style)
         | 
| 42 | 
            +
                      if style.is_a?(Symbol)
         | 
| 43 | 
            +
                        style_class = self.const_get(style.upcase)
         | 
| 44 | 
            +
                        return style_class if style_class.is_a?(Border::Style)
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
                      error_message = "Unsupported border style: #{style.inspect}"
         | 
| 47 | 
            +
                      raise ArgumentError, error_message
         | 
| 48 | 
            +
                    rescue NameError
         | 
| 49 | 
            +
                      error_message = "Unsupported border style: #{style.inspect}"
         | 
| 50 | 
            +
                      raise ArgumentError, error_message
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  attr_writer :left, :top, :right, :bottom, :inner_horiz, :inner_vert
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def left?
         | 
| 57 | 
            +
                    @left == true
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  def top?
         | 
| 61 | 
            +
                    @top == true
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  def right?
         | 
| 65 | 
            +
                    @right == true
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  def bottom?
         | 
| 69 | 
            +
                    @bottom == true
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def inner_horiz?
         | 
| 73 | 
            +
                    @inner_horiz == true
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  def inner_vert?
         | 
| 77 | 
            +
                    @inner_vert == true
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  def outer?
         | 
| 81 | 
            +
                    left? || top? || right? || bottom?
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  def style
         | 
| 85 | 
            +
                    @style || Styles::DEFAULT
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  def style=(val)
         | 
| 89 | 
            +
                    @style = Styles.validate!(val)
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  def color=(val)
         | 
| 93 | 
            +
                    @color = TextColor.validate!(val)
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  def color
         | 
| 97 | 
            +
                    @color || TextColor::DEFAULT
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
              end
         | 
| 101 | 
            +
            end
         | 
| @@ -0,0 +1,68 @@ | |
| 1 | 
            +
            require 'whirled_peas/utils/ansi'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module WhirledPeas
         | 
| 4 | 
            +
              module Settings
         | 
| 5 | 
            +
                # An abstract class that encapsulates colors for a specific use case
         | 
| 6 | 
            +
                class Color
         | 
| 7 | 
            +
                  # Validate the `color` argument is either (1) nil, (2) a valid Color constant in
         | 
| 8 | 
            +
                  # this class or (3) a symbol that maps to valid Color constant. E.g. if there
         | 
| 9 | 
            +
                  # is a RED constant in an implementing class, then :red or :bright_red are
         | 
| 10 | 
            +
                  # valid values for `color`
         | 
| 11 | 
            +
                  #
         | 
| 12 | 
            +
                  # @param color [Color|Symbol]
         | 
| 13 | 
            +
                  # @return [Color|Symbol] the value passed in if valid, otherwise an ArgumentError
         | 
| 14 | 
            +
                  #   is raised.
         | 
| 15 | 
            +
                  def self.validate!(color)
         | 
| 16 | 
            +
                    return if color.nil?
         | 
| 17 | 
            +
                    return color if color.is_a?(self)
         | 
| 18 | 
            +
                    if color.is_a?(Symbol)
         | 
| 19 | 
            +
                      match = color.to_s.match(/^(bright_)?(\w+)$/)
         | 
| 20 | 
            +
                      color_class = self.const_get(match[2].upcase)
         | 
| 21 | 
            +
                      if color_class.is_a?(self)
         | 
| 22 | 
            +
                        if !match[1]
         | 
| 23 | 
            +
                          return color_class
         | 
| 24 | 
            +
                        elsif !color_class.bright?
         | 
| 25 | 
            +
                          return color_class.bright
         | 
| 26 | 
            +
                        end
         | 
| 27 | 
            +
                      end
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                    error_message = "Unsupported #{self.name.split('::').last}: #{color.inspect}"
         | 
| 30 | 
            +
                    raise ArgumentError, error_message
         | 
| 31 | 
            +
                  rescue NameError
         | 
| 32 | 
            +
                    error_message = "Unsupported #{self.name.split('::').last}: #{color.inspect}"
         | 
| 33 | 
            +
                    raise ArgumentError, error_message
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def initialize(code, bright=false)
         | 
| 37 | 
            +
                    @code = code
         | 
| 38 | 
            +
                    @bright = bright
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  def bright?
         | 
| 42 | 
            +
                    @bright
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def bright
         | 
| 46 | 
            +
                    bright? ? self : self.class.new(@code + Utils::Ansi::BRIGHT_OFFSET, true)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def hash
         | 
| 50 | 
            +
                    [@code, @bright].hash
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def ==(other)
         | 
| 54 | 
            +
                    other.is_a?(self.class) && self.hash == other.hash
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                  alias_method :eq?, :==
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def to_s
         | 
| 59 | 
            +
                    @code.to_s
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  def inspect
         | 
| 63 | 
            +
                    "#{self.class.name.split('::').last}(code=#{@code}, bright=#{@bright})"
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
                private_constant :Color
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
            end
         |