Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ext/ratatui_ruby/src/rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub fn render_node(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), E
"RatatuiRuby::Widgets::Table" => widgets::table::render(buffer, area, node)?,
"RatatuiRuby::Widgets::Block" => widgets::block::render(buffer, area, node)?,
"RatatuiRuby::Widgets::Tabs" => widgets::tabs::render(buffer, area, node)?,
"RatatuiRuby::Widgets::ScrollView" => widgets::scroll_view::render(buffer, area, node)?,
"RatatuiRuby::Widgets::Scrollbar" => widgets::scrollbar::render(buffer, area, node)?,
"RatatuiRuby::Widgets::BarChart" => widgets::barchart::render(buffer, area, node)?,
"RatatuiRuby::Widgets::Canvas" => widgets::canvas::render(buffer, area, node)?,
Expand Down
1 change: 1 addition & 0 deletions ext/ratatui_ruby/src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod overlay;
pub mod paragraph;
pub mod ratatui_logo;
pub mod ratatui_mascot;
pub mod scroll_view;
pub mod scrollbar;
pub mod scrollbar_state;
pub mod sparkline;
Expand Down
62 changes: 62 additions & 0 deletions ext/ratatui_ruby/src/widgets/scroll_view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
// SPDX-License-Identifier: LGPL-3.0-or-later

use crate::rendering::render_node;
use magnus::{prelude::*, Error, Value};
use ratatui::{buffer::Buffer, layout::Rect};

pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
let child: Value = node.funcall("child", ())?;
let content_height: u16 = node.funcall("content_height", ())?;
let scroll_val: Value = node.funcall("scroll", ())?;

let scroll_y: u16 = if scroll_val.is_nil() {
0
} else {
let arr = magnus::RArray::from_value(scroll_val).ok_or_else(|| {
let ruby = magnus::Ruby::get().unwrap();
Error::new(
ruby.exception_type_error(),
"scroll must be [y, x] array or nil",
)
})?;
if arr.len() > 0 {
arr.entry::<u16>(0)?
} else {
0
}
};

// If no scrolling needed, render directly
if scroll_y == 0 && content_height <= area.height {
return render_node(buffer, area, child);
}

// Create virtual buffer tall enough for all content
let virtual_height = content_height.max(area.height);
let virtual_area = Rect::new(0, 0, area.width, virtual_height);
let mut virtual_buf = Buffer::empty(virtual_area);

// Render child widget tree into virtual buffer
render_node(&mut virtual_buf, virtual_area, child)?;

// Copy visible viewport from virtual buffer to real buffer
let clamped_scroll = scroll_y.min(virtual_height.saturating_sub(area.height));
for y in 0..area.height {
let src_y = y + clamped_scroll;
if src_y >= virtual_height {
break;
}
for x in 0..area.width {
if let Some(src_cell) = virtual_buf.cell((x, src_y)) {
if let Some(dst_cell) = buffer.cell_mut((area.x + x, area.y + y)) {
dst_cell
.set_symbol(src_cell.symbol())
.set_style(src_cell.style());
}
}
}
}

Ok(())
}
7 changes: 7 additions & 0 deletions lib/ratatui_ruby/tui/widget_factories.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ def axis(first = nil, **kwargs)
Widgets::Axis.coerce_args(first, kwargs)
end

# Creates a Widgets::ScrollView.
# @return [Widgets::ScrollView]
def scroll_view(first = nil, **kwargs)
Widgets::ScrollView.coerce_args(first, kwargs)
end

# Creates a Widgets::Scrollbar.
# @return [Widgets::Scrollbar]
def scrollbar(first = nil, **kwargs)
Expand Down Expand Up @@ -251,6 +257,7 @@ def widget(type, first = nil, **)
when :sparkline then sparkline(first, **)
when :bar_chart then bar_chart(first, **)
when :chart then chart(first, **)
when :scroll_view then scroll_view(first, **)
when :scrollbar then scrollbar(first, **)
when :calendar then calendar(first, **)
when :canvas then canvas(first, **)
Expand Down
1 change: 1 addition & 0 deletions lib/ratatui_ruby/widgets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module Widgets
require_relative "widgets/bar_chart/bar"
require_relative "widgets/bar_chart/bar_group"
require_relative "widgets/chart"
require_relative "widgets/scroll_view"
require_relative "widgets/scrollbar"
require_relative "widgets/calendar"
require_relative "widgets/canvas"
Expand Down
39 changes: 39 additions & 0 deletions lib/ratatui_ruby/widgets/scroll_view.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

#--
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
# SPDX-License-Identifier: LGPL-3.0-or-later
#++

module RatatuiRuby
module Widgets
# Scrolls arbitrary widget content by rendering to a virtual buffer
# and copying the visible viewport.
#
# Unlike Paragraph's built-in scroll, this works with any widget tree:
# layouts, nested blocks, styled text, tables, etc.
#
# === Examples
#
# ScrollView.new(
# child: tui.layout(direction: :vertical, ...),
# scroll: [scroll_y, 0],
# content_height: total_lines
# )
class ScrollView < Data.define(:child, :scroll, :content_height)
include CoerceableWidget

# Creates a new ScrollView.
#
# [child]
# The widget tree to scroll.
# [scroll]
# Scroll offset as [y, x]. Only vertical (y) is used currently.
# [content_height]
# Total height of the content in rows. Used to size the virtual buffer.
def initialize(child:, scroll: [0, 0], content_height: 0)
super
end
end
end
end