Scrolling Containers
When layouts contain lists or large blocks of content, they can overflow. RTXUI supports full horizontal and vertical scrolling with visual scrollbars.
Enabling Scroll
By adding overflow-y: scroll (or overflow-x: scroll) and restricting the dimensions (height or width), RTXUI automatically crops overflowing elements and displays a modern, responsive scrollbar.
class ScrollBox : public Component<ScrollBox> {
public:
std::string_view view = R"html(
<div class="scroll-container">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
<!-- ... -->
</div>
<style>
.scroll-container {
display: block;
height: 5;
overflow-y: scroll;
scrollbar-width: auto;
border: wide;
border-color: rgb(29, 78, 216);
}
</style>
)html";
};Event Bubbling
Scroll events automatically bubble up to nested containers when boundaries are reached, allowing for natural feeling nested scroll areas.
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Nested scroll containers and scroll chaining.
//
// An inner container consumes wheel events until it reaches its end, then the
// event propagates to its parent.
#include <rtxui/rtxui.hpp>
using namespace rtxui;
class NestedScrollDemo : public Component<NestedScrollDemo> {
public:
std::string_view view = R"html(
<h1>RTXUI Nested Scrolling & Chaining Demo</h1>
<p class="desc">
This demo showcases nested scrollable elements. Reaching the boundaries (top or bottom)
of an inner container will bubble the scroll event (mouse wheel or keyboard) to the outer container.
</p>
<div class="section-title">[Outer Start] Beginning of Outer List</div>
<div>[Outer Item 1] Scroll down to discover inner containers...</div>
<div>[Outer Item 2] Outer container can scroll independently.</div>
<div>[Outer Item 3] Scroll speed configured in CSS.</div>
<div>[Outer Item 4] Scrollbar dynamically renders on the right.</div>
<div>[Outer Item 5] Preparing to load Inner Box A...</div>
<div class="inner-title">Nested Scrollbox A (Light Blue Border)</div>
<div class="inner-scroll box-a">
<div>[Box A - Item 1] Focus me & press ArrowDown</div>
<div>[Box A - Item 2] Or scroll with mouse wheel</div>
<div>[Box A - Item 3] Standard scroll chaining is active</div>
<div>[Box A - Item 4] Item 4</div>
<div>[Box A - Item 5] Item 5</div>
<div>[Box A - Item 6] Item 6</div>
<div>[Box A - Item 7] Item 7</div>
<div>[Box A - Item 8] Item 8</div>
<div>[Box A - Item 9] Item 9</div>
<div>[Box A - Item 10] Item 10</div>
<div>[Box A - Item 11] Item 11</div>
<div>[Box A - Item 12] Item 12</div>
<div>[Box A - Item 13] Item 13</div>
<div>[Box A - Item 14] Item 14</div>
<div>[Box A - Item 15] Box A Bottom - Scroll down again to bubble up</div>
</div>
<div class="section-title">[Outer Middle] Content between inner containers</div>
<div>[Outer Item 6] This is intermediate content.</div>
<div>[Outer Item 7] Layout handles arbitrary nesting level.</div>
<div>[Outer Item 8] Ready for Inner Box B...</div>
<div class="inner-title">Nested Scrollbox B (Steel Blue Border)</div>
<div class="inner-scroll box-b">
<div>[Box B - Item 1] Welcome to Box B!</div>
<div>[Box B - Item 2] Focus me & scroll to end</div>
<div>[Box B - Item 3] Item 3</div>
<div>[Box B - Item 4] Item 4</div>
<div>[Box B - Item 5] Item 5</div>
<div>[Box B - Item 6] Item 6</div>
<div>[Box B - Item 7] Item 7</div>
<div>[Box B - Item 8] Item 8</div>
<div>[Box B - Item 9] Item 9</div>
<div>[Box B - Item 10] Item 10</div>
<div>[Box B - Item 11] Item 11</div>
<div>[Box B - Item 12] Item 12</div>
<div>[Box B - Item 13] Item 13</div>
<div>[Box B - Item 14] Item 14</div>
<div>[Box B - Item 15] Box B Bottom - Scroll down again to bubble up</div>
</div>
<div class="section-title">[Outer End] End of Outer List</div>
<div>[Outer Item 9] Both nested boxes can bubble scroll events up.</div>
<div>[Outer Item 10] Thank you for using RTXUI!</div>
<style>
self {
--surface: rgb(22, 27, 34);
--accent-bright: rgb(121, 192, 255);
display: block;
width: 100%;
height: 100%;
overflow-y: scroll;
scroll-speed: 1;
background-color: rgb(13, 17, 23);
color: rgb(230, 237, 243);
padding: 1;
}
h1 {
color: var(--accent-bright);
font-weight: bold;
margin-bottom: 1;
}
.desc {
color: rgb(139, 148, 158);
margin-bottom: 1;
}
.inner-scroll {
display: block;
height: 5;
overflow-y: scroll;
scroll-speed: 1;
margin-top: 1;
margin-bottom: 1;
}
.box-a {
border: tall;
border-color: var(--accent-bright);
background-color: var(--surface);
}
.box-b {
border: tall;
border-color: rgb(88, 166, 255);
background-color: var(--surface);
}
.section-title {
color: var(--accent-bright);
font-weight: bold;
margin-top: 1;
margin-bottom: 1;
}
.inner-title {
color: rgb(191, 219, 254);
font-weight: bold;
margin-top: 1;
}
</style>
)html";
};
int main() {
auto app = Ref<NestedScrollDemo>::New();
Screen screen(app);
screen.Loop();
return 0;
}Scroll-Into-View on Keyboard Focus
RTXUI automatically scrolls containers to keep focused elements visible when users navigate using the keyboard (Tab and Shift+Tab).
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Scroll-into-view on keyboard focus.
//
// Tabbing to an element that is outside its scroll container scrolls it into
// view automatically.
//
// Try it: hold Tab and watch the list follow the focus ring.
#include <rtxui/rtxui.hpp>
using namespace rtxui;
class FocusScrollDemo : public Component<FocusScrollDemo> {
public:
std::string_view view = R"html(
<div class="container">
<h2>Keyboard Focus Scroll-Into-View Demo</h2>
<p class="description">
Press <b>Tab</b> or <b>Shift-Tab</b> to cycle focus through the items below.
The scrollable container will automatically scroll to keep the focused element in view.
</p>
<div class="scroll-window">
<div class="list-item" tabindex="0">Item 1 (Start)</div>
<div class="list-item" tabindex="0">Item 2</div>
<div class="list-item" tabindex="0">Item 3</div>
<div class="list-item" tabindex="0">Item 4</div>
<div class="list-item" tabindex="0">Item 5</div>
<div class="list-item" tabindex="0">Item 6</div>
<div class="list-item" tabindex="0">Item 7</div>
<div class="list-item" tabindex="0">Item 8</div>
<div class="list-item" tabindex="0">Item 9</div>
<div class="list-item" tabindex="0">Item 10 (End)</div>
</div>
</div>
<style>
self {
--accent: rgb(88, 166, 255);
display: block;
padding: 1 2;
background-color: rgb(13, 17, 23); /* Deep dark slate background */
color: rgb(230, 237, 243);
}
h2 {
color: var(--accent); /* Bright blue */
margin-bottom: 0;
}
.description {
color: rgb(139, 148, 158); /* Muted gray text */
margin-bottom: 2;
}
.scroll-window {
display: block;
height: 6;
border: tall;
border-color: rgb(48, 54, 61);
overflow-y: scroll;
scroll-speed: 1;
width: 40;
}
.list-item {
display: block;
padding: 0 1;
border: solid;
border-color: rgb(22, 27, 34);
background-color: rgb(30, 41, 59, 0.4);
margin: 0;
}
.list-item:hover {
background-color: rgb(30, 41, 59, 0.8);
}
.list-item:focus {
border-color: var(--accent); /* Blue focus */
background-color: rgb(30, 58, 138, 0.5); /* Blue background tint */
color: rgb(255, 255, 255);
}
</style>
)html";
};
int main() {
auto app = Ref<FocusScrollDemo>::New();
Screen screen(app);
screen.Loop();
return 0;
}Horizontal Scrolling
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Horizontal overflow.
//
// overflow-x on a container that is narrower than its content produces a
// horizontal scrollbar.
//
// Try it: scroll with Shift+wheel, or drag the scrollbar.
#include <rtxui/rtxui.hpp>
using namespace rtxui;
class HorizontalScrollDemo : public Component<HorizontalScrollDemo> {
public:
std::string_view view = R"html(
<div class="header">
<h1>RTXUI Horizontal Scrolling Demo</h1>
<p class="desc">
This demo showcases horizontal scrollable elements. Scroll horizontally using Shift + mouse wheel,
WheelLeft/Right events, or ArrowLeft/ArrowRight keys when focused.
</p>
</div>
<div class="item first">[Outer Start]</div>
<div class="item">[Scroll right to find nested scrollboxes...]</div>
<div class="item">[Item A]</div>
<div class="item">[Item B]</div>
<div class="nested-container">
<div class="inner-title">Nested Box A (Light Blue Border)</div>
<div class="inner-scroll box-a">
<div class="inner-item first">[A-1]</div>
<div class="inner-item">[A-2]</div>
<div class="inner-item">[A-3]</div>
<div class="inner-item">[A-4]</div>
<div class="inner-item">[A-5]</div>
<div class="inner-item">[A-6]</div>
<div class="inner-item">[A-7]</div>
<div class="inner-item">[A-8]</div>
<div class="inner-item">[A-9]</div>
<div class="inner-item last">[A-End]</div>
</div>
</div>
<div class="item">[Middle Item]</div>
<div class="nested-container">
<div class="inner-title">Nested Box B (Steel Blue Border)</div>
<div class="inner-scroll box-b">
<div class="inner-item first">[B-1]</div>
<div class="inner-item">[B-2]</div>
<div class="inner-item">[B-3]</div>
<div class="inner-item">[B-4]</div>
<div class="inner-item">[B-5]</div>
<div class="inner-item">[B-6]</div>
<div class="inner-item">[B-7]</div>
<div class="inner-item">[B-8]</div>
<div class="inner-item">[B-9]</div>
<div class="inner-item last">[B-End]</div>
</div>
</div>
<div class="item last">[Outer End]</div>
<style>
self {
--surface: rgb(22, 27, 34);
--border: rgb(48, 54, 61);
--accent-bright: rgb(121, 192, 255);
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
overflow-x: scroll;
scroll-speed-x: 2;
background-color: rgb(13, 17, 23);
color: rgb(230, 237, 243);
padding: 1;
}
.header {
display: block;
width: 35;
height: 8;
flex-shrink: 0;
margin-right: 2;
border: tall;
border-color: var(--surface);
padding: 1;
}
h1 {
color: var(--accent-bright);
font-weight: bold;
margin-bottom: 1;
}
.desc {
color: rgb(139, 148, 158);
margin-bottom: 1;
}
.item {
display: block;
width: 25;
height: 8;
margin-right: 2;
background-color: var(--surface);
color: rgb(219, 234, 254);
border: tall;
border-color: var(--border);
padding: 1;
}
.nested-container {
display: block;
width: 40;
height: 8;
margin-right: 2;
}
.inner-title {
color: rgb(191, 219, 254);
font-weight: bold;
margin-bottom: 1;
}
.inner-scroll {
display: flex;
flex-direction: row;
width: 38;
height: 5;
overflow-x: scroll;
scroll-speed-x: 1;
}
.box-a {
border: tall;
border-color: var(--accent-bright);
background-color: var(--surface);
}
.box-b {
border: tall;
border-color: rgb(88, 166, 255);
background-color: var(--surface);
}
.inner-item {
display: block;
width: 10;
margin-right: 1;
background-color: rgb(15, 23, 42);
color: var(--accent-bright);
border: tall;
border-color: var(--border);
padding: 0 1;
}
.first {
color: rgb(74, 222, 128);
}
.last {
color: rgb(248, 81, 73);
}
</style>
)html";
};
int main() {
auto app = Ref<HorizontalScrollDemo>::New();
Screen screen(app);
screen.Loop();
return 0;
}Scroll Behavior
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// scroll-behavior: smooth versus auto.
//
// Try it: click the jump buttons and compare how each column travels.
#include <rtxui/rtxui.hpp>
using namespace rtxui;
class ScrollBehaviorDemo : public Component<ScrollBehaviorDemo> {
public:
std::string_view view = R"html(
<div class="container">
<div class="header">
<h1>RTXUI Scroll Behavior Demo</h1>
<p class="desc">
Compare the two scroll behaviors below using the mouse wheel, Arrow keys, or PageUp/PageDown.
</p>
</div>
<div class="row">
<div class="column">
<h2>scroll-behavior: auto (Instant Content)</h2>
<p class="subtitle">Content snaps instantly, scrollbar thumb glides smoothly.</p>
<div id="scroll-auto" class="scrollbox">
<div class="item">Item 1</div>
<div class="item">Item 2</div>
<div class="item">Item 3</div>
<div class="item">Item 4</div>
<div class="item">Item 5</div>
<div class="item">Item 6</div>
<div class="item">Item 7</div>
<div class="item">Item 8</div>
<div class="item">Item 9</div>
<div class="item">Item 10</div>
<div class="item">Item 11</div>
<div class="item">Item 12</div>
</div>
</div>
<div class="column">
<h2>scroll-behavior: smooth (Smooth Content)</h2>
<p class="subtitle">Both viewport content and scrollbar thumb glide smoothly.</p>
<div id="scroll-smooth" class="scrollbox">
<div class="item">Item 1</div>
<div class="item">Item 2</div>
<div class="item">Item 3</div>
<div class="item">Item 4</div>
<div class="item">Item 5</div>
<div class="item">Item 6</div>
<div class="item">Item 7</div>
<div class="item">Item 8</div>
<div class="item">Item 9</div>
<div class="item">Item 10</div>
<div class="item">Item 11</div>
<div class="item">Item 12</div>
</div>
</div>
</div>
</div>
<style>
self {
--border: rgb(48, 54, 61);
--muted: rgb(139, 148, 158);
display: block;
width: 80;
margin: 0 auto;
background-color: rgb(13, 17, 23);
color: rgb(230, 237, 243);
}
.container {
display: block;
padding: 2;
}
.header {
display: block;
margin-bottom: 2;
}
h1 {
color: rgb(121, 192, 255);
font-weight: bold;
margin-bottom: 1;
}
.desc {
color: var(--muted);
}
.row {
display: flex;
flex-direction: row;
width: 100%;
}
.column {
display: block;
flex-grow: 1;
width: 48%;
margin-right: 2%;
}
h2 {
color: rgb(224, 242, 254);
font-weight: bold;
margin-bottom: 1;
}
.subtitle {
color: var(--muted);
margin-bottom: 1;
height: 2;
}
.scrollbox {
display: block;
height: 10;
overflow-y: scroll;
border: tall;
border-color: var(--border);
background-color: rgb(22, 27, 34);
padding: 1;
}
#scroll-auto {
scroll-behavior: auto;
}
#scroll-smooth {
scroll-behavior: smooth;
}
.item {
display: block;
height: 2;
margin-bottom: 1;
background-color: var(--border);
color: rgb(255, 255, 255);
padding-left: 1;
}
</style>
)html";
};
int main() {
auto component = Ref<ScrollBehaviorDemo>::New();
Screen screen(component);
screen.Loop();
return 0;
}Anchor Navigation
RTXUI supports anchor navigation using <a> tags. When an <a> tag with a target hash (e.g. href="#sec-intro") is clicked, RTXUI automatically scrolls the element with the corresponding id (e.g. id="sec-intro") into view. Combined with position: sticky and scroll-behavior: smooth, this allows building robust sidebar navigation layouts.
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Anchor navigation.
//
// An <a href="#id"> scrolls its target into view inside the nearest scroll
// container.
//
// Try it: click the links in the index to jump between sections.
#include <rtxui/rtxui.hpp>
#include <string>
using namespace rtxui;
class AnchorDemo : public Component<AnchorDemo> {
public:
std::string_view view = R"html(
<div class="container">
<div class="header">
<h2>Anchor Navigation Demo</h2>
<p class="description">
Click the links in the sidebar to scroll the corresponding section into view.
</p>
</div>
<div class="workspace">
<!-- Sticky Sidebar Navigation -->
<div class="sidebar">
<div class="nav-title">SECTIONS</div>
<a class="nav-link" href="#sec-intro">Introduction</a>
<a class="nav-link" href="#sec-features">Features</a>
<a class="nav-link" href="#sec-install">Installation</a>
<a class="nav-link" href="#sec-usage">Usage</a>
<a class="nav-link" href="#sec-components">Components</a>
<a class="nav-link" href="#sec-docs">Documentation</a>
<a class="nav-link" href="#sec-faq">FAQ</a>
<a class="nav-link" href="#sec-contact">Contact</a>
</div>
<!-- Scrollable Content Pane -->
<div class="scroll-window">
<div id="sec-intro" class="section sec-odd">
<div class="section-title">Introduction</div>
<p>Welcome to RTXUI. This framework lets you build terminal user interfaces using familiar XML templates and CSS styles.</p>
<p>Layout features include block, inline, flexbox, grid, fixed, absolute, and sticky positioning.</p>
</div>
<div id="sec-features" class="section sec-even">
<div class="section-title">Features</div>
<p>• Declarative XML markup parsing</p>
<p>• Complete CSS layout and flexbox model</p>
<p>• Rich borders, margins, padding, and z-index</p>
<p>• Mouse support: hover, active, focus, and clicks</p>
</div>
<div id="sec-install" class="section sec-odd">
<div class="section-title">Installation</div>
<p>To use RTXUI in your CMake project, add the library using FetchContent:</p>
<p>FetchContent_Declare(rtxui GIT_REPOSITORY ...)</p>
<p>Then link it with target_link_libraries(your_target rtxui::rtxui).</p>
</div>
<div id="sec-usage" class="section sec-even">
<div class="section-title">Usage</div>
<p>Initialize a component class, define its view property with HTML/XML markup,</p>
<p>implement InitReflection(), and start the main Screen loop.</p>
<p>Bind C++ states to reactive template properties for dynamic UI updates.</p>
</div>
<div id="sec-components" class="section sec-odd">
<div class="section-title">Components</div>
<p>RTXUI supports custom reusable components. Standard built-in components</p>
<p>include divs, spans, inputs, buttons, sliders, textareas, and checkboxes.</p>
<p>Create nested hierarchies using standard XML slot definitions.</p>
</div>
<div id="sec-docs" class="section sec-even">
<div class="section-title">Documentation</div>
<p>Styles are resolved dynamically based on CSS selectors and active classes.</p>
<p>Use the C++ API to bind state variables and handle interactive events.</p>
</div>
<div id="sec-faq" class="section sec-odd">
<div class="section-title">FAQ</div>
<p>Q: Does it support mouse inputs? Yes, hovering and clicking are fully supported.</p>
<p>Q: Can I use CSS grid? Yes, grid-template-columns and grid-gap are available.</p>
<p>Q: Does it have animations? Yes, CSS transitions are supported.</p>
</div>
<div id="sec-contact" class="section sec-even">
<div class="section-title">Contact</div>
<p>Created by Arthur Sonzogni.</p>
<p>Licensed under the MIT License.</p>
</div>
</div>
</div>
</div>
<style>
self {
--border: rgb(48, 54, 61);
--muted: rgb(139, 148, 158);
display: block;
padding: 1 2;
background-color: rgb(13, 17, 23); /* Deep dark slate background */
color: rgb(230, 237, 243);
}
.container {
display: block;
max-width: 80;
width: 100%;
margin-left: auto;
margin-right: auto;
}
.header {
display: block;
margin-bottom: 1;
}
h2 {
color: rgb(88, 166, 255); /* Bright blue */
margin-bottom: 0;
}
.description {
color: var(--muted); /* Muted gray text */
}
.workspace {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 2;
width: 100%;
}
.sidebar {
position: sticky;
top: 0;
display: flex;
flex-direction: column;
width: 18;
border: tall;
border-color: var(--border);
background-color: rgb(30, 41, 59, 0.4);
padding: 1;
flex-shrink: 0;
z-index: 10;
}
.nav-title {
color: var(--muted);
font-weight: bold;
margin-bottom: 1;
}
.nav-link {
display: block;
color: rgb(56, 189, 248); /* Cyan link */
margin-bottom: 1;
padding-left: 1;
cursor: pointer;
}
.nav-link:hover {
background-color: rgb(30, 41, 59, 0.8);
color: white;
}
.scroll-window {
display: block;
height: 18;
border: tall;
border-color: var(--border);
overflow-y: scroll;
scroll-speed: 1;
scroll-behavior: smooth;
flex-grow: 1;
}
.section {
display: block;
margin: 2;
padding: 2;
}
.section-title {
font-weight: bold;
margin-bottom: 1;
}
.sec-odd {
background-color: rgba(30, 58, 138, 0.15); /* Deep slate royal blue */
}
.sec-even {
background-color: rgba(14, 165, 233, 0.15); /* Modern cyan sky blue */
}
.sec-odd .section-title {
color: rgb(121, 192, 255); /* Bright royal blue text */
}
.sec-even .section-title {
color: rgb(56, 189, 248); /* Bright sky blue text */
}
</style>
)html";
};
int main() {
auto app = Ref<AnchorDemo>::New();
Screen screen(app);
screen.Loop();
return 0;
}An application using all of it
A file browser: the listing scrolls, its rows are focusable, and moving the focus walks them while keeping the focused row on screen.
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// A complete application driven by the keyboard.
//
// Where app_dashboard.cpp is about composing layout and state, this one is
// about the two things a terminal UI lives or dies by: scrolling a list that
// is longer than the viewport, and moving a selection through it without a
// mouse. Rows carry `tabindex`, so RTXUI's built-in focus navigation walks
// them and scrolls the focused one into view; the preview pane re-renders
// from computed values as the selection moves.
//
// Try it: move with the arrow keys (or j/k via the buttons), Enter to open a
// directory, Backspace to go up. The list scrolls to follow the selection.
#include <rtxui/rtxui.hpp>
#include <string>
#include <vector>
using namespace rtxui;
namespace {
struct FileEntry {
std::string name;
std::string kind; // "dir" | "file"
std::string size;
std::string modified;
bool selected = false;
bool operator==(const FileEntry& other) const = default;
};
// A small fixed tree, so the example stays self-contained and deterministic
// rather than depending on whatever happens to be on disk.
struct Directory {
std::string path;
std::vector<FileEntry> entries;
};
const std::vector<Directory>& Tree() {
static const std::vector<Directory> tree = {
{"/",
{
{"src", "dir", "-", "2026-08-01"},
{"docs", "dir", "-", "2026-07-28"},
{"example", "dir", "-", "2026-08-09"},
{"CMakeLists.txt", "file", "14 kB", "2026-08-09"},
{"README.md", "file", "6.2 kB", "2026-08-09"},
{"LICENSE", "file", "1.1 kB", "2026-01-12"},
}},
{"/src",
{
{"rtxui", "dir", "-", "2026-08-09"},
{"dummy.cpp", "file", "42 B", "2026-03-04"},
}},
{"/docs",
{
{"guide", "dir", "-", "2026-08-09"},
{"index.md", "file", "2.4 kB", "2026-08-09"},
{"css_reference.md", "file", "31 kB", "2026-08-09"},
{"html_reference.md", "file", "18 kB", "2026-08-09"},
{"reactivity.md", "file", "7.8 kB", "2026-08-09"},
}},
{"/example",
{
{"app_dashboard.cpp", "file", "9.1 kB", "2026-08-09"},
{"app_filebrowser.cpp", "file", "8.4 kB", "2026-08-09"},
{"counter.cpp", "file", "2.8 kB", "2026-08-09"},
{"helloworld.cpp", "file", "1.4 kB", "2026-08-09"},
{"playground.cpp", "file", "7.0 kB", "2026-08-09"},
{"transitions.cpp", "file", "2.6 kB", "2026-08-09"},
{"grid.cpp", "file", "2.2 kB", "2026-08-09"},
{"lists.cpp", "file", "3.9 kB", "2026-08-09"},
{"table.cpp", "file", "6.1 kB", "2026-08-09"},
{"tooltip.cpp", "file", "4.3 kB", "2026-08-09"},
{"sticky.cpp", "file", "4.6 kB", "2026-08-09"},
{"markdown.cpp", "file", "6.8 kB", "2026-08-09"},
}},
};
return tree;
}
} // namespace
class FileBrowser : public Component<FileBrowser> {
public:
std::string path = "/";
int cursor = 0;
std::vector<FileEntry> entries = Tree().front().entries;
// --- Computed values -----------------------------------------------------
std::string current_path() const { return path; }
std::string entry_count() const {
return std::to_string(entries.size()) + " items";
}
std::string selected_name() const {
const FileEntry* entry = Selected();
return entry ? entry->name : "-";
}
std::string selected_kind() const {
const FileEntry* entry = Selected();
return entry ? (entry->kind == "dir" ? "Directory" : "File") : "-";
}
std::string selected_size() const {
const FileEntry* entry = Selected();
return entry ? entry->size : "-";
}
std::string selected_modified() const {
const FileEntry* entry = Selected();
return entry ? entry->modified : "-";
}
bool selected_is_dir() const {
const FileEntry* entry = Selected();
return entry && entry->kind == "dir";
}
bool can_go_up() const { return path != "/"; }
// --- Handlers ------------------------------------------------------------
void MoveDown() { SetCursor(cursor + 1); }
void MoveUp() { SetCursor(cursor - 1); }
void Open() {
const FileEntry* entry = Selected();
if (!entry || entry->kind != "dir") {
return;
}
std::string next =
path == "/" ? "/" + entry->name : path + "/" + entry->name;
Navigate(next);
}
void GoUp() {
if (path == "/") {
return;
}
size_t slash = path.find_last_of('/');
Navigate(slash == 0 ? "/" : path.substr(0, slash));
}
std::string_view view = R"html(
<div class="app">
<div class="header">
<span class="brand">FILES</span>
<span class="path">{current_path}</span>
<span class="spacer"></span>
<span class="count">{entry_count}</span>
</div>
<div class="body">
<div class="listing" tabindex="0">
<for each="{entries}" as="entry">
<div class="row {entry.row_class}" tabindex="0" onclick="Select({$index})">
<span class="glyph {entry.kind}">{entry.glyph}</span>
<span class="name">{entry.name}</span>
<span class="size">{entry.size}</span>
<span class="modified">{entry.modified}</span>
</div>
</for>
</div>
<div class="preview">
<span class="preview-name">{selected_name}</span>
<span class="preview-kind">{selected_kind}</span>
<div class="kv"><span class="k">Size</span><span class="v">{selected_size}</span></div>
<div class="kv"><span class="k">Modified</span><span class="v">{selected_modified}</span></div>
<if condition="{selected_is_dir}">
<button class="primary" onclick="Open">Open directory</button>
</if>
<if condition="{can_go_up}">
<button onclick="GoUp">Go up</button>
</if>
</div>
</div>
<div class="footer">
<button class="key" onclick="MoveUp">k / up</button>
<button class="key" onclick="MoveDown">j / down</button>
<span class="spacer"></span>
<span class="hint">the list scrolls to keep the selection visible</span>
</div>
</div>
<style>
self {
--bg: rgb(13, 17, 23);
--surface: rgb(22, 27, 34);
--raised: rgb(31, 38, 47);
--border: rgb(48, 54, 61);
--text: rgb(230, 237, 243);
--muted: rgb(139, 148, 158);
--accent: rgb(88, 166, 255);
display: block;
width: 100%;
height: 100%;
background-color: var(--bg);
color: var(--text);
}
.app {
display: flex;
flex-direction: column;
height: 100%;
}
.header {
display: flex;
align-items: center;
gap: 2;
background-color: var(--surface);
border-bottom: solid;
border-color: var(--border);
padding: 0 2;
}
.brand {
color: var(--accent);
font-weight: bold;
}
.path {
color: var(--text);
}
.count {
color: var(--muted);
}
.spacer {
flex-grow: 1;
}
.body {
display: flex;
flex-grow: 1;
gap: 1;
padding: 1 2;
}
/* The list is deliberately shorter than its content: scroll-behavior
smooth plus the browser-style scroll-into-view on focus is what
keeps the selected row visible as the cursor moves. */
.listing {
display: flex;
flex-direction: column;
flex-grow: 1;
border: tall;
border-color: var(--border);
background-color: var(--surface);
padding: 0 1;
overflow-y: scroll;
scroll-behavior: smooth;
}
.row {
display: flex;
gap: 1;
align-items: center;
padding: 0 1;
transition: background-color 0.15s ease;
}
.row:hover {
background-color: var(--raised);
}
.row.selected, .row:focus {
background-color: var(--raised);
border-left: tall;
border-color: var(--accent);
}
.glyph.dir {
color: rgb(210, 153, 34);
}
.glyph.file {
color: var(--muted);
}
.name {
flex-grow: 1;
}
.size {
width: 10;
color: var(--muted);
text-align: right;
}
.modified {
width: 12;
color: var(--muted);
text-align: right;
}
.preview {
display: flex;
flex-direction: column;
border: tall;
border-color: var(--border);
background-color: var(--surface);
padding: 1 2;
width: 30;
}
.preview-name {
color: var(--accent);
font-weight: bold;
}
.preview-kind {
color: var(--muted);
margin-bottom: 1;
}
.kv {
display: flex;
justify-content: space-between;
width: 100%;
}
.k { color: var(--muted); }
.v { font-weight: bold; }
button {
border: tall;
border-color: var(--border);
background-color: var(--bg);
color: var(--text);
padding: 0 1;
margin-top: 1;
text-align: center;
transition: background-color 0.15s ease, border-color 0.15s ease,
color 0.15s ease;
}
button:hover, button:focus {
border-color: var(--accent);
color: var(--accent);
}
button:active {
background-color: var(--accent);
color: var(--bg);
}
button.primary {
border-color: var(--accent);
color: var(--accent);
}
.footer {
display: flex;
align-items: center;
gap: 2;
border-top: solid;
border-color: var(--border);
background-color: var(--surface);
color: var(--muted);
padding: 0 2;
}
.footer button.key {
margin-top: 0;
}
.hint {
color: var(--border);
}
</style>
)html";
FileBrowser() {
BindCollection("entries", &entries, [](const FileEntry& entry) {
return std::make_shared<ManualStructVisitor>(
std::map<std::string, std::string, std::less<>>{
{"name", entry.name},
{"kind", entry.kind},
{"size", entry.size},
{"modified", entry.modified},
{"glyph", entry.kind == "dir" ? "/" : "."},
{"row_class", entry.selected ? "selected" : ""},
});
});
Bind(path);
Bind(cursor);
Bind(current_path);
Bind(entry_count);
Bind(selected_name);
Bind(selected_kind);
Bind(selected_size);
Bind(selected_modified);
Bind(selected_is_dir);
Bind(can_go_up);
Bind(MoveDown);
Bind(MoveUp);
Bind(Open);
Bind(GoUp);
Import("Select",
[this](std::string index_str) { SetCursor(std::stoi(index_str)); });
SetCursor(0);
}
// Arrow keys drive the cursor; Enter opens; Backspace goes up. Returning
// true marks the event handled so it doesn't also reach the focus system.
bool OnEvent(Event event) override {
if (!event.is<Event::Keyboard>()) {
return Component<FileBrowser>::OnEvent(event);
}
auto key = event.get<Event::Keyboard>();
if (key.motion != Event::Keyboard::Motion::Pressed &&
key.motion != Event::Keyboard::Motion::Repeat) {
return Component<FileBrowser>::OnEvent(event);
}
switch (key.special) {
case Event::Keyboard::Special::ArrowDown:
MoveDown();
return false; // let focus move too, so the list scrolls to follow
case Event::Keyboard::Special::ArrowUp:
MoveUp();
return false;
case Event::Keyboard::Special::Return:
Open();
return true;
case Event::Keyboard::Special::Backspace:
GoUp();
return true;
default:
break;
}
if (key.codepoint == 'j') {
MoveDown();
return true;
}
if (key.codepoint == 'k') {
MoveUp();
return true;
}
return Component<FileBrowser>::OnEvent(event);
}
private:
const FileEntry* Selected() const {
if (cursor < 0 || cursor >= static_cast<int>(entries.size())) {
return nullptr;
}
return &entries[cursor];
}
void SetCursor(int index) {
if (index < 0 || index >= static_cast<int>(entries.size())) {
return;
}
for (FileEntry& entry : entries) {
entry.selected = false;
}
entries[index].selected = true;
cursor = index;
}
void Navigate(const std::string& next) {
for (const Directory& directory : Tree()) {
if (directory.path == next) {
path = next;
entries = directory.entries;
SetCursor(0);
return;
}
}
}
};
int main() {
auto app = Ref<FileBrowser>::New();
Screen screen(app);
screen.Loop();
return 0;
}
