summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml82
-rw-r--r--.github/workflows/release-tip.yml30
-rw-r--r--Makefile5
-rw-r--r--compile_commands.json38
-rw-r--r--src/IOManager.cpp22
-rw-r--r--src/IOManager.hpp30
-rw-r--r--src/TODO.md11
-rw-r--r--src/chatManager.cpp15
-rw-r--r--src/chatManager.hpp5
-rw-r--r--src/chatServer.cpp14
-rw-r--r--src/inputManager.cpp64
-rw-r--r--src/inputManager.hpp14
-rw-r--r--src/log.hpp13
-rw-r--r--src/renderManager.cpp19
-rw-r--r--src/renderManager.hpp21
-rw-r--r--src/scroller.cpp85
-rw-r--r--src/scroller.hpp15
17 files changed, 402 insertions, 81 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a30df9b..7cb54ca 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,7 +29,12 @@ jobs:
- name: Install dependencies
run: |
pacman -Syu --noconfirm
- pacman -S --noconfirm base-devel git make ninja gcc clang
+ pacman -S --noconfirm base-devel git make ninja gcc clang cmake
+ git clone https://github.com/ArthurSonzogni/FTXUI.git
+ cd FTXUI
+ cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build
+ cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
+ cmake --install build
- name: Checkout repository
uses: actions/checkout@v4
@@ -49,39 +54,39 @@ jobs:
name: chat-server-linux
path: build/chat-server
- macos:
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
- name: "Build (macOS)"
- runs-on: macos-latest
- steps:
- - name: Install dependencies
- run: |
- brew update
- brew install llvm@14 gcc@14 make ninja
- - name: Install compiler
- id: install_cc
- uses: rlalik/setup-cpp-compiler@master
- with:
- compiler: gcc-14
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- ref: ${{ github.head_ref }}
- fetch-depth: 0
- - name: Build project
- env:
- # CC: ${{ steps.install_cc.outputs.cc }}
- # CXX: ${{ steps.install_cc.outputs.cxx }}
- # above don't work cuz for some reason it's using g++ from /usr/local/bin :/
- CC: /opt/homebrew/bin/g++-14
- CXX: /opt/homebrew/bin/g++-14
- run: make all
- - name: Upload Artifact
- if: success()
- uses: actions/upload-artifact@v4
- with:
- name: chat-server-macos
- path: build/chat-server
+ # macos:
+ # if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
+ # name: "Build (macOS)"
+ # runs-on: macos-latest
+ # steps:
+ # - name: Install dependencies
+ # run: |
+ # brew update
+ # brew install llvm@14 gcc@14 make ninja
+ # - name: Install compiler
+ # id: install_cc
+ # uses: rlalik/setup-cpp-compiler@master
+ # with:
+ # compiler: gcc-14
+ # - name: Checkout repository
+ # uses: actions/checkout@v4
+ # with:
+ # ref: ${{ github.head_ref }}
+ # fetch-depth: 0
+ # - name: Build project
+ # env:
+ # # CC: ${{ steps.install_cc.outputs.cc }}
+ # # CXX: ${{ steps.install_cc.outputs.cxx }}
+ # # above don't work cuz for some reason it's using g++ from /usr/local/bin :/
+ # CC: /opt/homebrew/bin/g++-14
+ # CXX: /opt/homebrew/bin/g++-14
+ # run: make all
+ # - name: Upload Artifact
+ # if: success()
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: chat-server-macos
+ # path: build/chat-server
clang-linux:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
@@ -93,7 +98,12 @@ jobs:
- name: Install dependencies
run: |
pacman -Syu --noconfirm
- pacman -S --noconfirm base-devel git make ninja clang llvm
+ pacman -S --noconfirm base-devel git make ninja clang llvm cmake
+ git clone https://github.com/ArthurSonzogni/FTXUI.git
+ cd FTXUI
+ cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build
+ cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
+ cmake --install build
- name: Checkout repository
uses: actions/checkout@v4
@@ -104,7 +114,7 @@ jobs:
- name: Build project
run: |
- CXX=clang++ make all
+ CXX=clang++ CC=clang++ make all
clang-format:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml
index 22e01ed..6283e7c 100644
--- a/.github/workflows/release-tip.yml
+++ b/.github/workflows/release-tip.yml
@@ -48,13 +48,13 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- - name: Download macOS Build Artifact
- uses: actions/download-artifact@v4
- with:
- name: chat-server-macos
- path: artifacts/macos/
- github-token: ${{ secrets.GITHUB_TOKEN }}
- run-id: ${{ github.event.workflow_run.id }}
+ # - name: Download macOS Build Artifact
+ # uses: actions/download-artifact@v4
+ # with:
+ # name: chat-server-macos
+ # path: artifacts/macos/
+ # github-token: ${{ secrets.GITHUB_TOKEN }}
+ # run-id: ${{ github.event.workflow_run.id }}
- name: Create Source Archive
run: |
@@ -72,13 +72,13 @@ jobs:
cd ../..
sha256sum chat-server-linux-x86_64.tar.gz > chat-server-linux-x86_64.tar.gz.sha256
- - name: Prepare macOS Binary Archive
- run: |
- cd artifacts/macos
- chmod +x chat-server
- tar -czv --owner=0 --group=0 --no-same-owner --no-same-permissions -f ../../chat-server-macos-x86_64.tar.gz *
- cd ../..
- sha256sum chat-server-macos-x86_64.tar.gz > chat-server-macos-x86_64.tar.gz.sha256
+ # - name: Prepare macOS Binary Archive
+ # run: |
+ # cd artifacts/macos
+ # chmod +x chat-server
+ # tar -czv --owner=0 --group=0 --no-same-owner --no-same-permissions -f ../../chat-server-macos-x86_64.tar.gz *
+ # cd ../..
+ # sha256sum chat-server-macos-x86_64.tar.gz > chat-server-macos-x86_64.tar.gz.sha256
- name: Release
uses: softprops/action-gh-release@v2
@@ -92,7 +92,5 @@ jobs:
chat-server-source.tar.gz.sha256
chat-server-linux-x86_64.tar.gz
chat-server-linux-x86_64.tar.gz.sha256
- chat-server-macos-x86_64.tar.gz
- chat-server-macos-x86_64.tar.gz.sha256
token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Makefile b/Makefile
index cac7f4e..6fdde0a 100644
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,11 @@ LDFLAGS += -Wl,--as-needed,-z,now,-z,pack-relative-relocs
DEBUG_CXXFLAGS = -std=c++26 -g3 -O0 -Wall -fno-lto -fPIC -DDEBUG
DEBUG_LDFLAGS = -Wl,--no-as-needed,-z,now,-z,pack-relative-relocs
+CXXFLAGS += $(shell pkg-config --cflags ftxui)
+LDFLAGS += $(shell pkg-config --libs ftxui)
+DEBUG_CXXFLAGS += $(shell pkg-config --cflags ftxui)
+DEBUG_LDFLAGS += $(shell pkg-config --libs ftxui)
+
$(shell mkdir -p $(BUILDDIR))
JOB_COUNT := $(BIN) $(OBJS)
JOBS_DONE := $(shell ls -l $(JOB_COUNT) 2> /dev/null | wc -l)
diff --git a/compile_commands.json b/compile_commands.json
index e4aab6a..628ff44 100644
--- a/compile_commands.json
+++ b/compile_commands.json
@@ -125,6 +125,25 @@
"-fPIC",
"-DDEBUG",
"-o",
+ "build/IOManager.o",
+ "src/IOManager.cpp"
+ ],
+ "directory": "/mnt/DATA/personal/Code/projects/chat-server",
+ "file": "/mnt/DATA/personal/Code/projects/chat-server/src/IOManager.cpp",
+ "output": "/mnt/DATA/personal/Code/projects/chat-server/build/IOManager.o"
+ },
+ {
+ "arguments": [
+ "/usr/bin/afl-g++",
+ "-c",
+ "-std=c++26",
+ "-g3",
+ "-O0",
+ "-Wall",
+ "-fno-lto",
+ "-fPIC",
+ "-DDEBUG",
+ "-o",
"build/main.o",
"src/main.cpp"
],
@@ -144,6 +163,25 @@
"-fPIC",
"-DDEBUG",
"-o",
+ "build/renderManager.o",
+ "src/renderManager.cpp"
+ ],
+ "directory": "/mnt/DATA/personal/Code/projects/chat-server",
+ "file": "/mnt/DATA/personal/Code/projects/chat-server/src/renderManager.cpp",
+ "output": "/mnt/DATA/personal/Code/projects/chat-server/build/renderManager.o"
+ },
+ {
+ "arguments": [
+ "/usr/bin/afl-g++",
+ "-c",
+ "-std=c++26",
+ "-g3",
+ "-O0",
+ "-Wall",
+ "-fno-lto",
+ "-fPIC",
+ "-DDEBUG",
+ "-o",
"build/server.o",
"src/server.cpp"
],
diff --git a/src/IOManager.cpp b/src/IOManager.cpp
new file mode 100644
index 0000000..c5363af
--- /dev/null
+++ b/src/IOManager.cpp
@@ -0,0 +1,22 @@
+#include "IOManager.hpp"
+#include "inputManager.hpp"
+#include "log.hpp"
+
+CIOManager::CIOManager() {
+ log(LOG, "IOManager: initialized");
+}
+
+CIOManager::~CIOManager() {
+ m_vIO.clear();
+ log(SYS, "IOManager: bye");
+}
+
+const std::vector<CIOManager::SIO> &CIOManager::getIO() const {
+ return m_vIO;
+}
+
+void CIOManager::addLog(const SLog &log) {
+ m_vIO.push_back({.msg = std::nullopt, .log = log});
+ if (g_pInputManager)
+ g_pInputManager->updateIO();
+}
diff --git a/src/IOManager.hpp b/src/IOManager.hpp
new file mode 100644
index 0000000..ba741ff
--- /dev/null
+++ b/src/IOManager.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "chatManager.hpp"
+#include "format.hpp"
+#include <optional>
+
+class CIOManager {
+ public:
+ CIOManager();
+ ~CIOManager();
+
+ struct SLog {
+ eFormatType type;
+ std::string log;
+ };
+ struct SIO {
+ // cannot have both at once
+ std::optional<CChatManager::SMessage> msg;
+ std::optional<SLog> log;
+ };
+
+ const std::vector<SIO> &getIO() const;
+ void addLog(const SLog &log);
+
+ private:
+ std::vector<SIO> m_vIO;
+
+ friend class CChatManager;
+};
+inline std::unique_ptr<CIOManager> g_pIOManager;
diff --git a/src/TODO.md b/src/TODO.md
new file mode 100644
index 0000000..381095c
--- /dev/null
+++ b/src/TODO.md
@@ -0,0 +1,11 @@
+## General
+- [ ] ci unit testing
+
+## FTXUI
+
+- [ ] tabs for each user for direct message
+- ~[ ] fix: bottom(input) only 20% of screen instead of hardcoded px~
+ - [x] fix: bottom(input) only take 1line of screen
+- [-] auto focus input // FIXME
+- [ ] tabs for logs(log/chat)
+- [-] sticky scroll(scroll to bottom)
diff --git a/src/chatManager.cpp b/src/chatManager.cpp
index 3b8c82d..96c1b0c 100644
--- a/src/chatManager.cpp
+++ b/src/chatManager.cpp
@@ -1,4 +1,6 @@
#include "chatManager.hpp"
+#include "IOManager.hpp"
+#include "inputManager.hpp"
#include "log.hpp"
#include "sessionManager.hpp"
@@ -7,7 +9,6 @@ CChatManager::CChatManager() {
}
CChatManager::~CChatManager() {
- m_vMessages.clear();
log(SYS, "ChatManager: bye");
}
@@ -19,13 +20,19 @@ void CChatManager::newMessage(const SMessage &msg) {
return;
}
g_pSessionManager->broadcastChat(msg);
- m_vMessages.push_back(msg);
+ g_pIOManager->m_vIO.push_back({.msg = msg, .log = std::nullopt});
+ g_pInputManager->updateIO();
}
std::string CChatManager::fmtBroadcastMessage(const SMessage &msg) {
return NFormatter::fmt(NONE, "{}: {}", msg.username, msg.msg);
}
-const std::vector<CChatManager::SMessage> &CChatManager::getChat() const {
- return m_vMessages;
+std::vector<CChatManager::SMessage> CChatManager::getChat() const {
+ std::vector<SMessage> chat;
+
+ for (const auto &[msg, log] : g_pIOManager->getIO())
+ if (msg)
+ chat.push_back(*msg);
+ return chat;
}
diff --git a/src/chatManager.hpp b/src/chatManager.hpp
index 084c002..0f423f7 100644
--- a/src/chatManager.hpp
+++ b/src/chatManager.hpp
@@ -16,9 +16,6 @@ class CChatManager {
void newMessage(const SMessage &msg);
std::string fmtBroadcastMessage(const SMessage &msg);
- const std::vector<SMessage> &getChat() const;
-
- private:
- std::vector<SMessage> m_vMessages;
+ std::vector<SMessage> getChat() const;
};
inline std::unique_ptr<CChatManager> g_pChatManager;
diff --git a/src/chatServer.cpp b/src/chatServer.cpp
index 56caf4b..7b64e79 100644
--- a/src/chatServer.cpp
+++ b/src/chatServer.cpp
@@ -1,9 +1,11 @@
#include "chatServer.hpp"
+#include "IOManager.hpp"
#include "chatManager.hpp"
#include "commandHandler.hpp"
#include "commands.hpp"
#include "inputManager.hpp"
#include "log.hpp"
+#include "renderManager.hpp"
#include "server.hpp"
#include "sessionManager.hpp"
@@ -28,18 +30,24 @@ void CChatServer::cleanup() {
g_pServer.reset();
g_pCommandHandler.reset();
g_pInputManager.reset();
+ g_pIOManager.reset();
+ g_pRenderManager.reset();
}
void CChatServer::initManagers() {
log(LOG, "ChatServer: initializing managers");
+ log(LOG, "Creating RenderManager");
+ g_pRenderManager = std::make_unique<CRenderManager>();
+ log(LOG, "Creating InputManager");
+ g_pInputManager = std::make_unique<CInputManager>();
log(LOG, "Creating Server");
g_pServer = std::make_unique<CServer>(m_port);
+ log(LOG, "Creating IOManager");
+ g_pIOManager = std::make_unique<CIOManager>();
log(LOG, "Creating SessionManager");
g_pSessionManager = std::make_shared<CSessionManager>();
log(LOG, "Creating ChatManager");
g_pChatManager = std::make_unique<CChatManager>();
- log(LOG, "Creating InputManager");
- g_pInputManager = std::make_unique<CInputManager>();
}
void CChatServer::start() {
@@ -53,5 +61,5 @@ void CChatServer::start() {
m_sessionManagerThread = std::jthread(&CSessionManager::run, g_pSessionManager.get());
m_sessionManagerThread.detach();
- g_pInputManager->run();
+ g_pInputManager->inputLoop();
}
diff --git a/src/inputManager.cpp b/src/inputManager.cpp
index 8de26bd..cc3880b 100644
--- a/src/inputManager.cpp
+++ b/src/inputManager.cpp
@@ -1,10 +1,43 @@
#include "inputManager.hpp"
+#include "IOManager.hpp"
#include "chatManager.hpp"
#include "commandHandler.hpp"
#include "log.hpp"
-#include <iostream>
+#include "renderManager.hpp"
+#include "scroller.hpp"
+
+CInputManager::CInputManager()
+ : m_input(ftxui::Input(&m_szInput, "Type here...", {.transform = [](ftxui::InputState state) {
+ state.element |= ftxui::bgcolor(ftxui::Color::HSVA(0, 0, 0, 0));
+ state.element |= ftxui::color(ftxui::Color::White);
+ return state.element; }, .multiline = false, .on_enter = [&] {
+ if (m_szInput.empty())
+ return;
+
+ g_pCommandHandler->isCommand(m_szInput) ? g_pCommandHandler->handleCommand(m_szInput) : g_pChatManager->newMessage({.msg = m_szInput, .username = "Server"});
+ m_szInput.clear(); }}) // NOLINT
+ )
+ , inputComponent(ftxui::Renderer(m_input, [&] {
+ return ftxui::hbox({
+ ftxui::text("> "),
+ m_input->Render(),
+ });
+ }))
+ , logComponent(ftxui::Renderer([] {
+ std::vector<ftxui::Element> messages;
+ for (const auto &[chat, log] : g_pIOManager->getIO()) {
+ if (chat && !chat->msg.empty()) {
+ std::string username = chat->username;
+ if (username.back() == '\n')
+ username.pop_back();
+ messages.push_back(ftxui::window(ftxui::text(username), ftxui::paragraph(chat->msg)));
+ } else if (log && !log->log.empty())
+ messages.push_back(ftxui::window(ftxui::text(NFormatter::fmt(log->type, "")), ftxui::paragraph(log->log)));
+ }
+
+ return ftxui::vbox(messages);
+ })) {
-CInputManager::CInputManager() {
log(LOG, "InputManager: initialized");
}
@@ -12,19 +45,24 @@ CInputManager::~CInputManager() {
log(SYS, "InputManager: bye");
}
-void CInputManager::run() {
- inputLoop();
+void CInputManager::updateIO() {
+ logComponent->OnEvent(ftxui::Event::Custom);
+ g_pRenderManager->screen.PostEvent(ftxui::Event::Custom);
}
void CInputManager::inputLoop() {
- while (true) {
- std::string input;
- std::print("> ");
- std::getline(std::cin, input);
-
- if (input.empty())
- continue;
+ container = logComponent |= ftxui::Scroller;
+ container = ftxui::ResizableSplitBottom(inputComponent, container, &bottomSize);
- g_pCommandHandler->isCommand(input) ? g_pCommandHandler->handleCommand(input) : g_pChatManager->newMessage({.msg = input, .username = "Server"});
- }
+ // FIXME: this shit spams TakeFocus(not ideal for power etc reasons)
+ // consider: custom loop, or just only allow focus to be inputComponent somehow
+ container |= ftxui::CatchEvent([&](ftxui::Event) {
+ inputComponent->TakeFocus();
+ return false;
+ });
+ g_pRenderManager->setRenderer(ftxui::Renderer(container, [this] {
+ return container->Render() | ftxui::border;
+ }));
+ inputComponent->TakeFocus();
+ g_pRenderManager->enterLoop();
}
diff --git a/src/inputManager.hpp b/src/inputManager.hpp
index ef75b52..a81e39d 100644
--- a/src/inputManager.hpp
+++ b/src/inputManager.hpp
@@ -1,5 +1,6 @@
#pragma once
+#include <ftxui/component/component.hpp>
#include <memory>
class CInputManager {
@@ -7,7 +8,18 @@ class CInputManager {
CInputManager();
~CInputManager();
- void run();
void inputLoop();
+
+ void updateIO();
+
+ private:
+ std::string m_szInput;
+
+ ftxui::Component m_input;
+ ftxui::Component inputComponent;
+ ftxui::Component logComponent;
+ ftxui::Component container;
+
+ int bottomSize = 1;
};
inline std::unique_ptr<CInputManager> g_pInputManager;
diff --git a/src/log.hpp b/src/log.hpp
index 996b6bc..a4654c4 100644
--- a/src/log.hpp
+++ b/src/log.hpp
@@ -1,16 +1,18 @@
#pragma once
+#include "IOManager.hpp"
#include "format.hpp"
-#include <format>
#include <print>
#include <string>
-#include <unistd.h>
inline void log(FILE *_stream, eFormatType type, std::string str) {
#ifndef DEBUG
if (type == TRACE)
return;
#endif
- std::println(_stream, "{}", NFormatter::fmt(type, str));
+ if (g_pIOManager)
+ g_pIOManager->addLog({.type = type, .log = str});
+ else
+ std::println(_stream, "{}", NFormatter::fmt(type, str));
}
template <typename... Args>
@@ -19,7 +21,10 @@ void log(FILE *_stream, eFormatType type, std::format_string<Args...> fmt, Args
if (type == TRACE)
return;
#endif
- std::println(_stream, "{}", NFormatter::fmt(type, fmt, std::forward<Args>(args)...));
+ if (g_pIOManager)
+ g_pIOManager->addLog({.type = type, .log = NFormatter::fmt(NONEWLINE, fmt, std::forward<Args>(args)...)});
+ else
+ std::println(_stream, "{}", NFormatter::fmt(type, fmt, std::forward<Args>(args)...));
}
inline void log(eFormatType type, std::string str) {
diff --git a/src/renderManager.cpp b/src/renderManager.cpp
new file mode 100644
index 0000000..d3720c7
--- /dev/null
+++ b/src/renderManager.cpp
@@ -0,0 +1,19 @@
+#include "renderManager.hpp"
+#include "log.hpp"
+
+CRenderManager::CRenderManager()
+ : screen(ftxui::ScreenInteractive::Fullscreen()) {
+ log(SYS, "RenderManager: initialized");
+};
+
+void CRenderManager::enterLoop() {
+ screen.Loop(renderer);
+}
+
+void CRenderManager::setRenderer(ftxui::Component component) {
+ renderer = std::move(component);
+}
+
+CRenderManager::~CRenderManager() {
+ screen.Exit();
+}
diff --git a/src/renderManager.hpp b/src/renderManager.hpp
new file mode 100644
index 0000000..4407f66
--- /dev/null
+++ b/src/renderManager.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <ftxui/component/screen_interactive.hpp>
+#include <memory>
+
+class CRenderManager {
+ public:
+ CRenderManager();
+ ~CRenderManager();
+
+ void enterLoop();
+
+ void setRenderer(ftxui::Component component);
+
+ private:
+ ftxui::ScreenInteractive screen;
+ ftxui::Component renderer;
+
+ friend class CInputManager;
+};
+inline std::unique_ptr<CRenderManager> g_pRenderManager;
diff --git a/src/scroller.cpp b/src/scroller.cpp
new file mode 100644
index 0000000..bf80770
--- /dev/null
+++ b/src/scroller.cpp
@@ -0,0 +1,85 @@
+#include "scroller.hpp"
+
+#include "log.hpp"
+#include <algorithm> // for max, min
+#include <ftxui/component/component_base.hpp> // for Component, ComponentBase
+#include <ftxui/component/event.hpp> // for Event, Event::ArrowDown, Event::ArrowUp, Event::End, Event::Home, Event::PageDown, Event::PageUp
+#include <memory> // for shared_ptr, allocator, __shared_ptr_access
+#include <utility> // for move
+
+#include "ftxui/component/component.hpp" // for Make
+#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::WheelDown, Mouse::WheelUp
+#include "ftxui/dom/deprecated.hpp" // for text
+#include "ftxui/dom/elements.hpp" // for operator|, Element, size, vbox, EQUAL, HEIGHT, dbox, reflect, focus, inverted, nothing, select, vscroll_indicator, yflex, yframe
+#include "ftxui/dom/node.hpp" // for Node
+#include "ftxui/dom/requirement.hpp" // for Requirement
+#include "ftxui/screen/box.hpp" // for Box
+
+namespace ftxui {
+
+class ScrollerBase : public ComponentBase {
+ public:
+ ScrollerBase(Component child) { Add(child); }
+
+ private:
+ Element OnRender() final {
+ auto focused = Focused() ? focus : ftxui::select;
+ auto style = Focused() ? inverted : nothing;
+
+ Element background = ComponentBase::Render();
+ background->ComputeRequirement();
+ size_ = background->requirement().min_y;
+ return dbox({
+ std::move(background),
+ vbox({
+ text(L"") | size(HEIGHT, EQUAL, selected_),
+ text(L"") | style | focused,
+ }),
+ }) |
+ vscroll_indicator | yframe | yflex | reflect(box_);
+ }
+
+ bool OnEvent(Event event) final {
+ if (event.is_mouse() && box_.Contain(event.mouse().x, event.mouse().y))
+ TakeFocus();
+
+ int selected_old = selected_;
+ if (event == Event::ArrowUp || event == Event::Character('k') ||
+ (event.is_mouse() && event.mouse().button == Mouse::WheelUp))
+ selected_--;
+ if ((event == Event::ArrowDown || event == Event::Character('j') ||
+ (event.is_mouse() && event.mouse().button == Mouse::WheelDown)))
+ selected_++;
+ if (event == Event::PageDown)
+ selected_ += box_.y_max - box_.y_min;
+ if (event == Event::PageUp)
+ selected_ -= box_.y_max - box_.y_min;
+ if (event == Event::Home)
+ selected_ = 0;
+ if (event == Event::End)
+ selected_ = size_;
+
+ if (event == Event::Custom && sticky_select)
+ selected_ = size_;
+
+ selected_ = std::max(0, std::min(size_, selected_));
+ sticky_select = selected_ == size_;
+ return selected_old != selected_;
+ }
+
+ bool Focusable() const final { return true; }
+
+ bool sticky_select = true;
+ int selected_ = 0;
+ int size_ = 0;
+ Box box_;
+};
+
+Component Scroller(Component child) {
+ return Make<ScrollerBase>(std::move(child));
+}
+} // namespace ftxui
+
+// Copyright 2021 Arthur Sonzogni. All rights reserved.
+// Use of this source code is governed by the MIT license that can be found in
+// the LICENSE file.
diff --git a/src/scroller.hpp b/src/scroller.hpp
new file mode 100644
index 0000000..81a2a6d
--- /dev/null
+++ b/src/scroller.hpp
@@ -0,0 +1,15 @@
+#ifndef SCROLLER_H
+#define SCROLLER_H
+
+#include <ftxui/component/component.hpp>
+
+#include "ftxui/component/component_base.hpp" // for Component
+
+namespace ftxui {
+Component Scroller(Component child);
+}
+#endif /* end of include guard: SCROLLER_H */
+
+// Copyright 2021 Arthur Sonzogni. All rights reserved.
+// Use of this source code is governed by the MIT license that can be found in
+// the LICENSE file.