WidgetTabs

Demonstrates view segregation with interactive tab navigation.

Screen real estate is limited. You cannot show everything at once. Segregating content into views is necessary for complex apps.

This demo showcases the Tabs widget. It provides an interactive playground where you can select tabs, cycle through dividers and styles, and adjust padding in real-time.

Use it to understand how to build major mode switches or context navigation for your interface.

Example

Run the demo from the terminal:

ruby examples/widget_tabs/app.rb

Source Code

# frozen_string_literal: true

#--
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
# SPDX-License-Identifier: MIT-0
#++

$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
require "ratatui_ruby"
require "faker"

# Demonstrates view segregation with interactive tab navigation.
#
# Screen real estate is limited. You cannot show everything at once. Segregating content into views is necessary for complex apps.
#
# This demo showcases the <tt>Tabs</tt> widget. It provides an interactive playground where you can select tabs, cycle through dividers and styles, and adjust padding in real-time.
#
# Use it to understand how to build major mode switches or context navigation for your interface.
#
# === Example
#
# Run the demo from the terminal:
#
#   ruby examples/widget_tabs/app.rb
#
# rdoc-image:/doc/images/widget_tabs.png
class WidgetTabs
  def initialize
    @selected_tab = 0
    @tabs = ["Revenue", "Traffic", "Errors", "Quarterly"]
    @highlight_styles = nil
    @highlight_style_index = 0
    @divider_index = 0
    @dividers = [" | ", " • ", " > ", " / "]
    @base_styles = nil
    @base_style_index = 0
    @padding_left = 0
    @padding_right = 0
    @width_constraint_index = 0
    @hotkey_style = nil

    # Generate the content once, not on every frame
    @tab_text = 4.times.map { |it| Faker::Lorem.paragraph(sentence_count: 10 + it) }
  end

  def run
    RatatuiRuby.run do |tui|
      @tui = tui
      init_styles

      loop do
        render
        break if handle_input == :quit
      end
    end
  end

  private def init_styles
    @highlight_styles = [
      { name: "Yellow Bold", style: @tui.style(fg: :yellow, modifiers: [:bold]) },
      { name: "Italic Blue on White", style: @tui.style(fg: :blue, bg: :white, modifiers: [:italic]) },
      { name: "Underlined Red", style: @tui.style(fg: :red, modifiers: [:underlined]) },
      { name: "Reversed", style: @tui.style(modifiers: [:reversed]) },
    ]
    @base_styles = [
      { name: "Default", style: nil },
      { name: "White on Gray", style: @tui.style(fg: :white, bg: :dark_gray) },
      { name: "White on Blue", style: @tui.style(fg: :white, bg: :blue) },
      { name: "Italic", style: @tui.style(modifiers: [:italic]) },
    ]
    @hotkey_style = @tui.style(modifiers: [:bold, :underlined])
  end

  private def render
    @tui.draw do |frame|
      main_area, controls_area = @tui.layout_split(
        frame.area,
        direction: :vertical,
        constraints: [
          @tui.constraint_fill(1),
          @tui.constraint_length(5),
        ]
      )

      # Center the tabs vertically in the main area
      tabs_area, content_area = @tui.layout_split(
        main_area,
        direction: :vertical,
        constraints: [
          @tui.constraint_length(3),
          @tui.constraint_fill(1),
        ]
      )

      tabs = @tui.tabs(
        titles: @tabs,
        selected_index: @selected_tab,
        block: @tui.block(title: "Tabs", borders: [:all]),
        divider: @dividers[@divider_index],
        highlight_style: @highlight_styles[@highlight_style_index][:style],
        style: @base_styles[@base_style_index][:style],
        padding_left: @padding_left,
        padding_right: @padding_right
      )
      frame.render_widget(tabs, tabs_area)
      frame.render_widget(tab_contents, content_area)

      render_controls(frame, controls_area, tabs.width)
    end
  end

  private def render_controls(frame, area, current_width)
    controls = @tui.block(
      title: "Controls",
      borders: [:all],
      children: [
        @tui.paragraph(
          text: [
            @tui.text_line(spans: [
              @tui.text_span(content: "←/→", style: @hotkey_style),
              @tui.text_span(content: ": Select Tab  "),
              @tui.text_span(content: "h/l", style: @hotkey_style),
              @tui.text_span(content: ": Pad Left (#{@padding_left})  "),
              @tui.text_span(content: "j/k", style: @hotkey_style),
              @tui.text_span(content: ": Pad Right (#{@padding_right})  "),
              @tui.text_span(content: "q", style: @hotkey_style),
              @tui.text_span(content: ": Quit"),
            ]),
            @tui.text_line(spans: [
              @tui.text_span(content: "d", style: @hotkey_style),
              @tui.text_span(content: ": Divider (#{@dividers[@divider_index]})  "),
              @tui.text_span(content: "s", style: @hotkey_style),
              @tui.text_span(content: ": Highlight (#{@highlight_styles[@highlight_style_index][:name]})  "),
              @tui.text_span(content: "b", style: @hotkey_style),
              @tui.text_span(content: ": Base Style (#{@base_styles[@base_style_index][:name]})  "),
            ]),
            @tui.text_line(spans: [
              @tui.text_span(content: "Width: #{current_width}"),
            ]),
          ]
        ),
      ]
    )
    frame.render_widget(controls, area)
  end

  private def handle_input
    case @tui.poll_event
    in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
      :quit
    in type: :key, code: "right"
      @selected_tab = (@selected_tab + 1) % @tabs.size
    in type: :key, code: "left"
      @selected_tab = (@selected_tab - 1) % @tabs.size
    in type: :key, code: "d"
      @divider_index = (@divider_index + 1) % @dividers.size
    in type: :key, code: "s"
      @highlight_style_index = (@highlight_style_index + 1) % @highlight_styles.size
    in type: :key, code: "b"
      @base_style_index = (@base_style_index + 1) % @base_styles.size
    in type: :key, code: "h"
      @padding_left = [@padding_left - 1, 0].max
    in type: :key, code: "l"
      @padding_left += 1
    in type: :key, code: "j"
      @padding_right = [@padding_right - 1, 0].max
    in type: :key, code: "k"
      @padding_right += 1
    else
      # Ignore other events
    end
  end

  private def tab_contents
    @tui.paragraph(
      text: @tab_text[@selected_tab],
      wrap: true,
      block: @tui.block(borders: [:all], title: @tabs[@selected_tab])
    )
  end
end

WidgetTabs.new.run if __FILE__ == $0