101 Commits

Author SHA1 Message Date
ArthurSonzogni
09eb2f7fb0 v6.0.2 2025-03-30 01:27:57 +01:00
Arthur Sonzogni
1144e13125 Apply @forworldm code review. (#1022)
See: https://github.com/ArthurSonzogni/FTXUI/pull/1021?notification_referrer_id=NT_kwDOAEieQrMxNTU3OTg4MDA1MDo0NzU5MTA2#discussion_r2019827970
2025-03-30 01:22:17 +01:00
Arthur Sonzogni
4ba7dd2c5e Window: Major crash fix. (#1021)
A patch handling focus was recently merged, but a special condition on
Windows was inverted, causing a segfault.

Bug:https://github.com/ArthurSonzogni/FTXUI/issues/1020
2025-03-29 12:51:08 +01:00
ArthurSonzogni
ee24bec3ba v6.0.1
Same as v6.0.0.

Due to a problem tag v6.0.0 was replaced. This isn't a good practice and affect
developers that started using it in the short timeframe. Submitting a new
release with the same content is the best way to fix this.

Bug:https://github.com/ArthurSonzogni/FTXUI/issues/1017
Bug:https://github.com/ArthurSonzogni/FTXUI/issues/1019
2025-03-28 12:08:59 +01:00
ArthurSonzogni
327f43b175 v6.0.0 2025-03-27 19:19:44 +01:00
Arthur Sonzogni
5bf8ee819b Update README.md 2025-03-23 23:55:31 +01:00
Arthur Sonzogni
d5b741b2be Update README.md 2025-03-23 19:26:36 +01:00
ArthurSonzogni
b69e0f8b91 v6.0.0 2025-03-23 18:19:57 +01:00
Arthur Sonzogni
67163c2571 Fix errors. (#1010) 2025-03-23 15:29:01 +01:00
KenReneris
2c9a828402 Add support for italics (#1009)
Co-authored-by: Ken Reneris <ms/devops kreneris@microsoft.com>
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2025-03-22 18:03:43 +01:00
ArthurSonzogni
bc682d25a6 Fix compiler nits. 2025-03-22 17:31:27 +01:00
Arthur Sonzogni
96e8b8d92e Implement Node::Select for flexbox. (#977) 2025-03-21 16:15:25 +01:00
ArthurSonzogni
f2fb434e31 Quickfix 2025-03-20 19:59:59 +01:00
Ayaan
b0e087ecef Merge dom and component focus (#978)
Instead of two levels of focus with `focus` and `selected`, use a recursive
level. The components set the one "active" and hbox/vbox/dbox 

Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2025-03-19 15:33:05 +01:00
Emmanuel Ogie
8519e9b0f3 Add terminal-rain and keywords to README.md (#1003)
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2025-03-13 12:24:57 +01:00
Frames
36c669c194 Add a new example in README.md (#1005)
Add project to README.

Add https://github.com/Cyxuan0311/FTB.git
2025-03-13 12:17:48 +01:00
Arthur Sonzogni
d75108e960 Fix linear_gradient float precision bug.
This was reported by:
https://github.com/ArthurSonzogni/FTXUI/issues/998

Indeed, the `t` interpolation factor, which is itself interpolated might become
slightly larger than 1.0. This is due to the float precision.
This was supposedly handled, but there was an off-by-one error in the check.

Along the way, fix a bug found by a fuzzer.

Bug: https://github.com/ArthurSonzogni/FTXUI/issues/998
Fixed: https://github.com/ArthurSonzogni/FTXUI/issues/998
2025-02-10 23:10:27 +01:00
Yazid
15587dad01 Adding BestEdrOfTheMarket in examples (#995) 2025-01-27 18:21:59 +01:00
s1dd
c58a234f05 [DOCS] Add inLimbo as example project (#988) 2025-01-20 15:29:54 +01:00
Jan Stranik
c89569f5a7 Update menu.cpp - remove unused variable (#982) 2025-01-03 18:20:11 +01:00
Arthur Sonzogni
f6a690a942 Fix dev warning. (#980) 2024-12-29 10:24:17 +01:00
Clément Roblot
6fafa2dfed Feature: Selection
Add support for selection content in the dom.
2024-12-27 09:45:13 +01:00
Arthur Sonzogni
751c8fab26 Force reload in examples. (#974)
After installing the service worker to use the COOP/COEP header, ensure
the document is reloaded.

Bug:https://github.com/ArthurSonzogni/FTXUI/issues/973
Fixed:https://github.com/ArthurSonzogni/FTXUI/issues/973
2024-12-25 13:32:35 +01:00
Sumit Patel
daa421fa6a add tuisic example project (#967) 2024-12-15 18:58:08 +01:00
Yongqi Zhu
e213cfda37 add Lazylist example project (#964) 2024-12-06 15:20:17 +01:00
Vemy
58ff448e76 Fix: Properly changing window title text color #940 (#961) 2024-12-01 09:38:09 +01:00
Dmitry Nefedov
dfa461b46b Clear terminal output of interactive screen on resize if alternate screen not in use (#952) 2024-11-27 21:52:20 +01:00
Dmitry Atamanov
0683285f01 Remove non-existent Just-Fast from README.md (#957) 2024-11-27 21:47:09 +01:00
glebundiy
7f74917887 Add yafth as an example project using FTXUI (#958) 2024-11-27 21:44:32 +01:00
Brian
ad0392ec39 Fixed typo on border (#956)
Fixed minor issue in function name
2024-11-20 22:37:02 +01:00
Boris Jaulmes
70bc44d28b Allow a Dimension::Fit to extend beyond the terminal maximum height (#950)
For long tables (and other DOM elements), one may want the screen to render on dimensions higher than the terminal.  
Hence, this PR proposes a way to do so, with an optional parameter in the `Dimension::Fit` util function.

Discussions / Issues :  
- https://github.com/ArthurSonzogni/FTXUI/issues/572
- https://github.com/ArthurSonzogni/FTXUI/discussions/949

Bug:https://github.com/ArthurSonzogni/FTXUI/issues/572
Fixed:Bug:https://github.com/ArthurSonzogni/FTXUI/issues/572
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2024-11-07 21:07:09 +01:00
Petr Vyazovik
55af678fb9 Added pciex as an example project using FTXUI (#948) 2024-11-02 12:08:43 +01:00
Mikołaj Lubiak
edaa7a24e7 Add memory game and terminal animation to project list (#946) 2024-10-31 21:09:42 +01:00
Vemy
8922e6d55e Add 2048-cpp to projects using FTXUI (#944) 2024-10-29 09:15:07 +01:00
Mikołaj Lubiak
99df1ac8ba Add SliderWithCallback component (#938)
Add SliderOption::on_change.

Useful to observe a change to the value.

Signed-off-by: Mikołaj Lubiak <lubiak@proton.me>
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2024-10-29 08:03:59 +01:00
Herring
1d40687a40 Add index to EntryState (#933)
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2024-09-30 23:18:59 +02:00
ljrrjl
dfb9558eaf add ftxui-image-view (#924) 2024-08-27 14:35:15 +02:00
ArthurSonzogni
c5357acbaa Add scrollbar example. 2024-08-18 10:46:41 +02:00
ArthurSonzogni
fbd56cdf43 Fix CQ failures. 2024-08-17 12:01:43 +02:00
ArthurSonzogni
66d1c1f61f Quickfix 2024-08-16 11:47:01 +02:00
Arthur Sonzogni
f5d8c7deb5 Apply Clang-tidy (#918) 2024-08-16 11:19:51 +02:00
Sergey Latu
535290bb3b My project added to readme (#916) 2024-08-15 16:08:32 +02:00
Arthur Sonzogni
fcd050c017 Table: support initializer list constructor. (#915)
To avoid burdening the user with explicit type construction when using
the library, we can use a constructor that accepts an initializer list
(std::initializer_list). This allows users to pass initializer lists
directly without having to wrap them in
std::vector<std::vector<std::string>>. This resolves the ambiguous case
when the inner list contains only two elements.

Bug:https://github.com/ArthurSonzogni/FTXUI/issues/912
2024-08-13 15:55:09 +02:00
Paolo Bosetti
d7de24cd9e Added -fPIC compile option (#913)
Added -fPIC compile option.
2024-08-11 19:17:57 +02:00
Charney Kaye
547d9278d8 Add XJ music to FTXUI example projects (#909)
We use FTXUI as the frontend for the example C++ application for our adaptive music runtime engine.

https://github.com/xjmusic/xjmusic/tree/main/engine/example
2024-08-04 11:05:59 +02:00
Brian
5a9ef876a1 Update README.md (#905)
added step writer
2024-07-25 11:07:38 +02:00
LiAuTraver
307e4eb4b3 add missing include guard for screen/pixel.hpp (#890) 2024-06-27 13:07:10 +02:00
sAkuraOfficial
b28d57086a fix a small bug in button example (#868) 2024-06-13 18:43:49 +02:00
Arthur Sonzogni
ff305147ca Color alpha support. (#884) 2024-06-13 18:43:14 +02:00
Timon Ensel
d6a2049483 Add ostree-tui to README (#874) 2024-06-09 15:50:01 +02:00
Dr Power
b5e11ba1f6 Added Caravan to README.md (#871) 2024-06-09 15:44:12 +02:00
ArthurSonzogni
a715a767b5 Fix Color::HSV(h,0,v)
There was a problem when v==0
2024-06-02 12:03:41 +02:00
Felix
7b1f4d435b Solve issues with atomic copy (#867) 2024-05-26 15:28:05 +02:00
Arthur Sonzogni
ecacb22d37 Dropdown: Fix title not updated. (#851)
A bug was introduced by:
https://github.com/ArthurSonzogni/FTXUI/pull/826

The checkbox label wasn't updated.

Bug:https://github.com/ArthurSonzogni/FTXUI/issues/861
2024-05-15 18:23:59 +02:00
ArthurSonzogni
af49b57e60 Dropdown: Fix title not updated.
A bug was introduced by:
https://github.com/ArthurSonzogni/FTXUI/pull/826

The checkbox label wasn't updated.

Bug:https://github.com/ArthurSonzogni/FTXUI/issues/861
2024-05-13 10:53:11 +02:00
ccn
4913379625 Update index.html (#858)
correct spelling
2024-05-06 12:54:17 +02:00
ccn
d40cafde5c Update homescreen.cpp (#859)
fix typo
2024-05-06 12:53:56 +02:00
ccn
65296b9aa3 Update flex.cpp (#860)
fix typo
2024-05-06 12:53:37 +02:00
ccn
a58e6e6bcf Update README.md (#857)
minor typo
2024-05-05 18:11:07 +02:00
Arthur Sonzogni
8a2a9b0799 Generate compile commands for clangd. (#855)
Fix all the diagnostics reported.

Bug: https://github.com/ArthurSonzogni/FTXUI/issues/828
2024-05-01 14:32:22 +02:00
Arthur Sonzogni
6a755f3760 Fix Menu focus. (#850)
Bug:https://github.com/ArthurSonzogni/FTXUI/issues/841
2024-04-28 16:03:00 +02:00
Jørn Gustav Larsen
d386df6f94 Enable raw keyboard input (#832)
In order for applications to receive all keyboard inputs, including the
Ctrl-C and Ctrl-Z, the raw input mode has been enabled. As result the
SIGINT will no longer be used, instead the keyboard Ctrl-C event is used
for exiting the framework, but only if no components has made use of it.

Co-authored-by: Jørn Gustav Larsen <jgl@fasttracksoftware.com>
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2024-04-28 15:17:54 +02:00
Mark Antabi
d38b14ffb6 Allow user to specify window element border. (#849) 2024-04-28 14:48:02 +02:00
ArthurSonzogni
7e3e1d4bca Apply clang-tidy. 2024-04-28 10:40:57 +02:00
Clancy Walters
affa787244 Prefer Exit() over OnExit() (#847)
This is a no-op patch, but prefered, because this centralize the exit path below `Exit()`.
2024-04-27 11:32:46 +02:00
Arthur Sonzogni
014bdb4a05 Flush before applying a new configuration. (#848)
This avoids an ordering problem with whatever the user printed and
interacting with termios/WinAPI.

Bug:https://github.com/ArthurSonzogni/FTXUI/issues/846
2024-04-27 11:18:35 +02:00
Dimo Markov
293ff179f6 Separate a reusable Image class from Screen (#834)
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2024-04-27 11:03:44 +02:00
cole-io
1f6e1101e8 clarified README and added tip on linking (#845)
Clarified some sentences, changed "external package" section to "utilization", added a tip on linking
2024-04-22 08:48:32 +02:00
na-trium-144
0dfd59bd09 Fix ResizableSplit handling keyboard navigation incorrectly (#842)
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2024-04-18 17:28:28 +02:00
ArthurSonzogni
e03a0797be Fix minor compile error. 2024-04-07 18:10:52 +02:00
James
3c9fa60d28 Feature: Dropdown options with callback (#826)
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2024-04-06 17:45:10 +02:00
Jørn Gustav Larsen
2216f3a5da Problem with setting the cursor position on the right screen edge when drawing. (#831)
When moving the cursor back to its original location, a problem arises when cursor placed in the right edge column, where an off by one error occur. This pull request will resolve this problem.

Co-authored-by: Jørn Gustav Larsen <jgl@fasttracksoftware.com>
Co-authored-by: Jørn Gustav Larsen <jgl@adminbyrequest.com>
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2024-04-03 21:32:19 +02:00
Arthur Sonzogni
f609c12846 Revert change to button example. (#835)
It was introduced mistakenly by:
f495ce029c
2024-03-30 11:01:28 +01:00
faizan171997
ce5ac6b12f - Added exit button to homescreen example (#819) 2024-02-22 12:12:51 +01:00
ArthurSonzogni
f495ce029c Add example to use system ftxui
Fixed: https://github.com/ArthurSonzogni/FTXUI/issues/814
2024-01-26 18:32:44 +01:00
Arthur Sonzogni
810657dab8 Update mainpage.md
Fixed: https://github.com/ArthurSonzogni/FTXUI/issues/812
2024-01-25 11:56:25 +01:00
rio
65bbb4f0eb Make Checkbox take focus when clicked (#810) 2024-01-17 18:21:32 +01:00
Arthur Sonzogni
5112d9139d Button: invoke on_click at the end. (#807)
Some users might destroy `this`, which would result in UAF.

In the future, we should consider alternatives like posting a task to
the main loop, or rely on users for this.

Fixed:https://github.com/ArthurSonzogni/FTXUI/issues/804
2024-01-10 22:08:57 +01:00
Mohammad Rahimi
91a162a30e Add FTowerX to README (#805)
FTowerX is Tower of Hanoi game developed using FTXUI
2024-01-08 16:48:42 +01:00
Nikola Dućak
4d5cc41c65 Add Captain's log to README (#803) 2024-01-08 07:25:38 +01:00
nyako
cc3bcbf069 ftxui_set_options: properly check the current compiler. (#802)
This solve the issue encountered when using clang under MSVC.
2024-01-08 07:05:41 +01:00
Particle_G
d0634e1ca0 Add missing Checkbox() implementation (#796)
Fix: #795
2023-12-23 08:35:20 +01:00
Arthur Sonzogni
a7b6785420 Restore cursor shape on exit. (#793) (#794)
Fixed: https://github.com/ArthurSonzogni/FTXUI/issues/792
2023-12-17 10:35:21 +01:00
Arthur Sonzogni
348c3853d4 Restore cursor shape on exit. (#793)
Fixed: https://github.com/ArthurSonzogni/FTXUI/issues/792
2023-12-17 10:24:33 +01:00
ArthurSonzogni
bfadcb7165 Fix default for ScreenInteractive::Fullscreen()
It was intended to open gthe alternate screen.
2023-11-19 14:09:42 +01:00
Arthur Sonzogni
6c2b43a2aa Improve the example page. (#780) 2023-11-19 12:09:52 +01:00
Arthur Sonzogni
b970cb6ea8 feature: allow fullscreen without alternative screen (#777)
This should solve #766

The original PR was:
#767

Co-authored-by: rbrugo <brugo.riccardo@gmail.com>
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2023-11-11 17:57:07 +01:00
Clément Roblot
c31aecf2ed Checkbox button debounce (#774)
This fixes: https://github.com/ArthurSonzogni/FTXUI/issues/773

Dragging the mouse with the left button pressed now avoids activating multiple
checkboxes.

Add support for detecting mouse press transition. Added:
```cpp
// The previous mouse event.
Mouse Mouse::previous;

// Return whether the mouse transitionned from:
// released to pressed => IsPressed()
// pressed to pressed => IsHeld()
// pressed to released => IsReleased()
bool Mouse::IsPressed(Button button) const;
bool Mouse::IsHeld(Button button) const;
bool Mouse::IsReleased(Button button) const;
```
A couple of components are now activated when the mouse is pressed,
as opposed to released.

Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2023-11-11 17:33:50 +01:00
chrysante
e8589dd533 Fix Input onchange not called (#776) 2023-11-11 17:29:19 +01:00
Clément Roblot
0631c3ab3f Add example to filter characters inputted in an input field (#763)
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2023-11-05 10:26:12 +01:00
Ali Caglayan
d548a18658 update nix and add dev shell (#769)
We update the lock on the nix flake and also add a dev shell. This means
you can do `nix build` to build the project and `nix develop` to drop
into a development environment with cmake and clang.

Signed-off-by: Ali Caglayan <alizter@gmail.com>
Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
2023-10-23 08:48:50 +02:00
benoitdudu
f499d34f7e fix the doxygen documentation by moving comments at the right place (#768) 2023-10-19 16:58:02 -04:00
Ruebled
d4c9c5e226 Update README.md (#761)
Version FetchContent Update
2023-10-12 17:27:18 +02:00
Clément Roblot
62c0b43caf Scrollbar coloring (#755)
This a proposed MR to fix #754. While building the scroll bar the pixels were completely reseted thus canceling any style previously applied to said pixels. This MR removes this resetting of the pixels and leaves only the drawing of the shape of the scroll bar.
2023-10-02 10:29:23 +02:00
Arthur Sonzogni
c24a274292 Feature: hscroll_indicator (#753)
This is the symetrical of `vscroll_indicator`.

Requested by @ibrahimnasson.

Fixed:https://github.com/ArthurSonzogni/FTXUI/issues/752
2023-09-26 23:08:42 +02:00
Contexploit
20d4be286b Example: fix small flaw in example/button.cpp (#751) 2023-09-20 18:23:05 +02:00
Arthur Sonzogni
550a59f0a5 Add catalincd/resource-monitor (#745) 2023-09-18 20:10:48 +02:00
Arthur Sonzogni
5db2be0f4d List J0sephDavis/ftxuiFileReader (#746) 2023-09-18 20:09:45 +02:00
Arthur Sonzogni
8d1665022a List ftxui_CPUMeter (#744) 2023-09-03 16:21:13 +02:00
Arthur Sonzogni
e5978a8e76 Update README.md 2023-09-03 11:45:23 +02:00
MingSheng
0f1588e3d1 Add grid container library (#742) 2023-09-02 11:00:45 +02:00
84 changed files with 2328 additions and 815 deletions

View File

@@ -19,18 +19,16 @@ jobs:
- name: Linux GCC
os: ubuntu-latest
compiler: gcc
gcov_executable: gcov
- name: Linux Clang
os: ubuntu-latest
compiler: llvm
gcov_executable: "llvm-cov gcov"
# https://github.com/aminya/setup-cpp/issues/246
#- name: MacOS clang
#os: macos-latest
#compiler: llvm
#gcov_executable: "llvm-cov gcov"
- name: MacOS clang
os: macos-latest
compiler: llvm
gcov_executable: "llvm-cov gcov"
- name: Windows MSVC
os: windows-latest
@@ -85,7 +83,7 @@ jobs:
ctest -C Debug --rerun-failed --output-on-failure;
- name: Unix - coverage
if: runner.os != 'Windows'
if: matrix.gcov_executable != ''
working-directory: ./build
run: >
gcovr
@@ -220,6 +218,8 @@ jobs:
rsync -amv
--include='*/'
--include='*.html'
--include='*.css'
--include='*.mjs'
--include='*.js'
--include='*.wasm'
--exclude='*'

View File

@@ -1,76 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '45 22 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'cpp' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

10
.gitignore vendored
View File

@@ -38,14 +38,16 @@ out/
!doc/**/*.md
# examples directory:
!examples/**/*.txt
!examples/**/*.cpp
!examples/**/*.css
!examples/**/*.hpp
!examples/**/*.ipp
!examples/**/*.html
!examples/**/*.py
!examples/**/*.js
!examples/**/*.html.disabled
!examples/**/*.ipp
!examples/**/*.js
!examples/**/*.mjs
!examples/**/*.py
!examples/**/*.txt
# include directory:
!include/ftxui/**/*.hpp

View File

@@ -1,8 +1,26 @@
Changelog
=========
current (development)
---------------------
6.0.2 (2025-03-30)
-----
### Component
- BugFix: Fix major crash on Windows affecting all components. See #1020
- BugFix: Fix focusRelative.
6.0.1 (2025-03-28)
-----
Same as v6.0.0.
Due to a problem tag v6.0.0 was replaced. This isn't a good practice and affect
developers that started using it in the short timeframe. Submitting a new
release with the same content is the best way to fix this.
See #1017 and #1019.
6.0.0 (2025-03-23)
-----
### Component
- Feature: Add support for raw input. Allowing more keys to be detected.
@@ -16,6 +34,9 @@ current (development)
- Feature: Add support for `Input`'s insert mode. Add `InputOption::insert`
option. Added by @mingsheng13.
- Feature: Add `DropdownOption` to configure the dropdown. See #826.
- Feature: Add support for Selection. Thanks @clement-roblot. See #926.
- See `ScreenInteractive::GetSelection()`.
- See `ScreenInteractive::SelectionChange(...)` listener.
- Bugfix/Breaking change: `Mouse transition`:
- Detect when the mouse move, as opposed to being pressed.
The Mouse::Moved motion was added.
@@ -35,16 +56,43 @@ current (development)
- Bugfix: Fix cursor position in when in the last column. See #831.
- Bugfix: Fix `ResizeableSplit` keyboard navigation. Fixed by #842.
- Bugfix: Fix `Menu` focus. See #841
- Feature: Add `ComponentBase::Index()`. This allows to get the index of a
component in its parent. See #932
- Feature: Add `EntryState::index`. This allows to get the index of a menu entry.
See #932
- Feature: Add `SliderOption::on_change`. This allows to set a callback when the
slider value changes. See #938.
- Bugfix: Handle `Dropdown` with no entries.
- Bugfix: Fix crash in `LinearGradient` due to float precision and an off-by-one
mistake. See #998.
### Dom
- Feature: Add `italic` decorator. For instance:
```cpp
auto italic_text = text("Italic text") | italic;
```
```cpp
auto italic_text = italic(text("Italic text"));
```
Proposed by @kenReneris in #1009.
- Feature: Add `hscroll_indicator`. It display an horizontal indicator
reflecting the current scroll position. Proposed by @ibrahimnasson in
[issue 752](https://github.com/ArthurSonzogni/FTXUI/issues/752)
- Feature: Add `extend_beyond_screen` option to `Dimension::Fit(..)`, allowing
the element to be larger than the screen. Proposed by @LordWhiro. See #572 and
#949.
- Feature: Add support for Selection. Thanks @clement-roblot. See #926.
- See `selectionColor` decorator.
- See `selectionBackgroundColor` decorator.
- See `selectionForegroundColor` decorator.
- See `selectionStyle(style)` decorator.
- See `selectionStyleReset` decorator.
- Breaking change: Change how "focus"/"select" are handled. This fixes the
behavior.
- Breaking change: `Component::OnRender()` becomes the method to override to
render a component. This replaces `Component::Render()` that is still in use
to call the rendering method on the children. This change allows to fix a
couple of issues around focus handling.
### Screen
- Feature: Add `Box::IsEmpty()`.

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.12)
project(ftxui
LANGUAGES CXX
VERSION 5.0.0
VERSION 6.0.2
DESCRIPTION "C++ Functional Terminal User Interface."
)
@@ -56,11 +56,12 @@ add_library(dom
include/ftxui/dom/flexbox_config.hpp
include/ftxui/dom/node.hpp
include/ftxui/dom/requirement.hpp
include/ftxui/dom/selection.hpp
include/ftxui/dom/take_any_args.hpp
src/ftxui/dom/automerge.cpp
src/ftxui/dom/selection_style.cpp
src/ftxui/dom/blink.cpp
src/ftxui/dom/bold.cpp
src/ftxui/dom/hyperlink.cpp
src/ftxui/dom/border.cpp
src/ftxui/dom/box_helper.cpp
src/ftxui/dom/box_helper.hpp
@@ -81,13 +82,16 @@ add_library(dom
src/ftxui/dom/graph.cpp
src/ftxui/dom/gridbox.cpp
src/ftxui/dom/hbox.cpp
src/ftxui/dom/hyperlink.cpp
src/ftxui/dom/inverted.cpp
src/ftxui/dom/italic.cpp
src/ftxui/dom/linear_gradient.cpp
src/ftxui/dom/node.cpp
src/ftxui/dom/node_decorator.cpp
src/ftxui/dom/paragraph.cpp
src/ftxui/dom/reflect.cpp
src/ftxui/dom/scroll_indicator.cpp
src/ftxui/dom/selection.cpp
src/ftxui/dom/separator.cpp
src/ftxui/dom/size.cpp
src/ftxui/dom/spinner.cpp

View File

@@ -43,7 +43,7 @@ A simple cross-platform C++ library for terminal based user interfaces!
* **Cross platform**: Linux/MacOS (main target), WebAssembly, Windows (Thanks to contributors!).
* Learn by [examples](#documentation), and [tutorials](#documentation)
* Multiple packages: CMake [FetchContent]([https://bewagner.net/programming/2020/05/02/cmake-fetchcontent/](https://cmake.org/cmake/help/latest/module/FetchContent.html)) (preferred), vcpkg, pkgbuild, conan.
* Good practises: documentation, tests, fuzzers, performance tests, automated CI, automated packaging, etc...
* Good practices: documentation, tests, fuzzers, performance tests, automated CI, automated packaging, etc...
## Documentation
@@ -73,7 +73,7 @@ A simple cross-platform C++ library for terminal based user interfaces!
#### DOM
This module defines a hierarchical set of Element. An element manages layout and can be responsive to the terminal dimensions.
This module defines a hierarchical set of Element. An Element manages layout and can be responsive to the terminal dimensions.
They are declared in [<ftxui/dom/elements.hpp>](https://arthursonzogni.github.io/FTXUI/elements_8hpp_source.html
)
@@ -109,6 +109,7 @@ Element can become flexible using the the `flex` decorator.
An element can be decorated using the functions:
- `bold`
- `italic`
- `dim`
- `inverted`
- `underlined`
@@ -199,7 +200,7 @@ Complex [examples](https://github.com/ArthurSonzogni/FTXUI/blob/master/examples/
#### Component
The ftxui/component is needed when you want to produce dynamic UI, reactive to the user's input. It defines a set of ftxui::Component. A component reacts to Events (keyboard, mouse, resize, ...) and Render Element (see previous section).
ftxui/component produces dynamic UI, reactive to the user's input. It defines a set of ftxui::Component. A component reacts to Events (keyboard, mouse, resize, ...) and Renders as an Element (see previous section).
Prebuilt components are declared in [<ftxui/component/component.hpp>](https://arthursonzogni.github.io/FTXUI/component_8hpp_source.html)
@@ -293,7 +294,10 @@ Prebuilt components are declared in [<ftxui/component/component.hpp>](https://ar
</details>
## Libraries for FTXUI
- *Want to share a useful component using FTXUI? Feel free adding yours here*
- *Want to share a useful Component for FTXUI? Feel free to add yours here*
- [ftxui-grid-container](https://github.com/mingsheng13/grid-container-ftxui)
- [ftxui-ip-input](https://github.com/mingsheng13/ip-input-ftxui)
- [ftxui-image-view](https://github.com/ljrrjl/ftxui-image-view.git): For Image Display.
## Project using FTXUI
@@ -301,12 +305,12 @@ Prebuilt components are declared in [<ftxui/component/component.hpp>](https://ar
Feel free to add your projects here:
- [json-tui](https://github.com/ArthurSonzogni/json-tui)
- [git-tui](https://github.com/ArthurSonzogni/git-tui)
- [ostree-tui](https://github.com/AP-Sensing/ostree-tui)
- [rgb-tui](https://github.com/ArthurSonzogni/rgb-tui)
- [chrome-log-beautifier](https://github.com/ArthurSonzogni/chrome-log-beautifier)
- [x86-64 CPU Architecture Simulation](https://github.com/AnisBdz/CPU)
- [ltuiny](https://github.com/adrianoviana87/ltuiny)
- [i3-termdialogs](https://github.com/mibli/i3-termdialogs)
- [Just-Fast](https://github.com/GiuseppeCesarano/just-fast)
- [simpPRU](https://github.com/VedantParanjape/simpPRU)
- [Pigeon ROS TUI](https://github.com/PigeonSensei/Pigeon_ros_tui)
- [hastur](https://github.com/robinlinden/hastur)
@@ -323,6 +327,27 @@ Feel free to add your projects here:
- [eCAL monitor](https://github.com/eclipse-ecal/ecal)
- [Path Finder](https://github.com/Ruebled/Path_Finder)
- [rw-tui](https://github.com/LeeKyuHyuk/rw-tui)
- [resource-monitor](https://github.com/catalincd/resource-monitor)
- [ftxuiFileReader](https://github.com/J0sephDavis/ftxuiFileReader)
- [ftxui_CPUMeter](https://github.com/tzzzzzzzx/ftxui_CPUMeter)
- [Captain's log](https://github.com/nikoladucak/caps-log)
- [FTowerX](https://github.com/MhmRhm/FTowerX)
- [Caravan](https://github.com/r3w0p/caravan)
- [Step-Writer](https://github.com/BrianAnakPintar/step-writer)
- [XJ music](https://github.com/xjmusic/xjmusic)
- [UDP chat](https://github.com/Sergeydigl3/udp-chat-tui)
- [2048-cpp](https://github.com/Chessom/2048-cpp)
- [Memory game](https://github.com/mikolajlubiak/memory)
- [Terminal Animation](https://github.com/mikolajlubiak/terminal_animation)
- [pciex](https://github.com/s0nx/pciex)
- [Fallout terminal hacking](https://github.com/gshigin/yet-another-fallout-terminal-hacking-game)
- [Lazylist](https://github.com/zhuyongqi9/lazylist)
- [TUISIC](https://github.com/Dark-Kernel/tuisic)
- [inLimbo](https://github.com/nots1dd/inLimbo)
- [BestEdrOfTheMarket](https://github.com/Xacone/BestEdrOfTheMarket)
- [terminal-rain](https://github.com/Oakamoore/terminal-rain)
- [keywords](https://github.com/Oakamoore/keywords) ([Play web version :heart:](https://oakamoore.itch.io/keywords))
- [FTB - tertminal file browser](https://github.com/Cyxuan0311/FTB)
### [cpp-best-practices/game_jam](https://github.com/cpp-best-practices/game_jam)
@@ -339,16 +364,15 @@ Several games using the FTXUI have been made during the Game Jam:
- [smoothlife](https://github.com/cpp-best-practices/game_jam/blob/main/Jam1_April_2022/smoothlife.md)
- [Consu](https://github.com/cpp-best-practices/game_jam/blob/main/Jam1_April_2022/consu.md)
## External package
## Utilization
It is **highly** recommended to use CMake FetchContent to depend on FTXUI. This
way you can specify which commit you would like to depend on.
It is **highly** recommended to use CMake FetchContent to depend on FTXUI so you may specify which commit you would like to depend on.
```cmake
include(FetchContent)
FetchContent_Declare(ftxui
GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui
GIT_TAG v3.0.0
GIT_TAG v6.0.2
)
FetchContent_GetProperties(ftxui)
@@ -358,13 +382,20 @@ if(NOT ftxui_POPULATED)
endif()
```
If you don't, the following packages have been created:
If you don't, FTXUI may be used from the following packages:
- [vcpkg](https://vcpkgx.com/details.html?package=ftxui)
- [Arch Linux PKGBUILD](https://aur.archlinux.org/packages/ftxui-git/).
- [conan.io](https://conan.io/center/ftxui)
- [openSUSE](https://build.opensuse.org/package/show/devel:libraries:c_c++/ftxui)
-
[![Packaging status](https://repology.org/badge/vertical-allrepos/libftxui.svg)](https://repology.org/project/libftxui/versions)
If you choose to build and link FTXUI yourself, `ftxui-component` must be first in the linking order relative to the other FTXUI libraries, i.e.
```bash
g++ . . . -lftxui-component -lftxui-dom -lftxui-screen . . .
```
[![Packaging status](https://repology.org/badge/vertical-allrepos/ftxui.svg)](https://repology.org/project/ftxui/versions)
## Contributors

View File

@@ -83,10 +83,6 @@ function(ftxui_set_options library)
target_compile_options(${library} PRIVATE "-Wpedantic")
target_compile_options(${library} PRIVATE "-Wshadow")
target_compile_options(${library} PRIVATE "-Wunused")
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(${library} PRIVATE "-Wuseless-cast")
endif()
endif()
endif()

View File

@@ -13,6 +13,7 @@ add_executable(ftxui-tests
src/ftxui/component/component_test.cpp
src/ftxui/component/component_test.cpp
src/ftxui/component/container_test.cpp
src/ftxui/component/dropdown_test.cpp
src/ftxui/component/hoverable_test.cpp
src/ftxui/component/input_test.cpp
src/ftxui/component/menu_test.cpp
@@ -38,8 +39,10 @@ add_executable(ftxui-tests
src/ftxui/dom/gridbox_test.cpp
src/ftxui/dom/hbox_test.cpp
src/ftxui/dom/hyperlink_test.cpp
src/ftxui/dom/italic_test.cpp
src/ftxui/dom/linear_gradient_test.cpp
src/ftxui/dom/scroll_indicator_test.cpp
src/ftxui/dom/selection_test.cpp
src/ftxui/dom/separator_test.cpp
src/ftxui/dom/spinner_test.cpp
src/ftxui/dom/table_test.cpp

View File

@@ -50,42 +50,17 @@ int main(void) {
└────┘└────────────────────────────────────┘└─────┘
```
# Build {#build}
## Configure {#configure}
### Using CMake and find_package {#build-cmake-find-package}
## Using CMake {#build-cmake}
Assuming FTXUI is available or installed on the system.
This is an example configuration for your **CMakeLists.txt**
CMakeLists.txt
**CMakeLists.txt**
```cmake
cmake_minimum_required (VERSION 3.11)
# --- Fetch FTXUI --------------------------------------------------------------
include(FetchContent)
set(FETCHCONTENT_UPDATES_DISCONNECTED TRUE)
FetchContent_Declare(ftxui
GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui
# Important: Specify a GIT_TAG XXXXX here.
GIT_TAG main
)
FetchContent_GetProperties(ftxui)
if(NOT ftxui_POPULATED)
FetchContent_Populate(ftxui)
add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()
# ------------------------------------------------------------------------------
project(ftxui-starter
LANGUAGES CXX
VERSION 1.0.0
)
find_package(ftxui 5 REQUIRED)
project(ftxui-starter LANGUAGES CXX VERSION 1.0.0)
add_executable(ftxui-starter src/main.cpp)
target_include_directories(ftxui-starter PRIVATE src)
target_link_libraries(ftxui-starter
PRIVATE ftxui::screen
PRIVATE ftxui::dom
@@ -94,7 +69,33 @@ target_link_libraries(ftxui-starter
```
Subsequently, you build the project in the standard fashion as follows:
### Using CMake and FetchContent {#build-cmake}
If you want to fetch FTXUI using cmake:
**CMakeLists.txt**
```cmake
cmake_minimum_required (VERSION 3.11)
include(FetchContent)
set(FETCHCONTENT_UPDATES_DISCONNECTED TRUE)
FetchContent_Declare(ftxui
GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui
GIT_TAG main # Important: Specify a version or a commit hash here.
)
FetchContent_MakeAvailable(ftxui)
project(ftxui-starter LANGUAGES CXX VERSION 1.0.0)
add_executable(ftxui-starter src/main.cpp)
target_link_libraries(ftxui-starter
PRIVATE ftxui::screen
PRIVATE ftxui::dom
PRIVATE ftxui::component # Not needed for this example.
)
```
## Build
```bash
mkdir build && cd build
cmake ..
@@ -122,8 +123,8 @@ The project is comprised of 3 modules:
This is the visual element of the program. It defines a `ftxui::Screen`, which
is a grid of `ftxui::Pixel`. A Pixel represents a Unicode character and its
associated style (bold, colors, etc.). The screen can be printed as a string
using `ftxui::Screen::ToString()`. The following example highlights this
associated style (bold, italic, colors, etc.). The screen can be printed as a
string using `ftxui::Screen::ToString()`. The following example highlights this
process:
```cpp
@@ -475,10 +476,11 @@ See [demo](https://arthursonzogni.github.io/FTXUI/examples/?file=component/linea
## Style {#dom-style}
In addition to colored text and colored backgrounds. Many terminals support text
effects such as: `bold`, `dim`, `underlined`, `inverted`, `blink`.
effects such as: `bold`, `italic`, `dim`, `underlined`, `inverted`, `blink`.
```cpp
Element bold(Element);
Element italic(Element);
Element dim(Element);
Element inverted(Element);
Element underlined(Element);
@@ -634,6 +636,26 @@ Produced by: `ftxui::Input()` from "ftxui/component/component.hpp"
<script id="asciicast-223719" src="https://asciinema.org/a/223719.js" async></script>
@endhtmlonly
### Filtered input
On can filter out the characters received by the input component, using
`ftxui::CatchEvent`.
```cpp
std::string phone_number;
Component input = Input(&phone_number, "phone number");
// Filter out non-digit characters.
input |= CatchEvent([&](Event event) {
return event.is_character() && !std::isdigit(event.character()[0]);
});
// Filter out characters past the 10th one.
input |= CatchEvent([&](Event event) {
return event.is_character() && phone_number.size() >= 10;
});
```
## Menu {#component-menu}
Defines a menu object. It contains a list of entries, one of them is selected.

View File

@@ -21,6 +21,8 @@ if (EMSCRIPTEN)
get_property(EXAMPLES GLOBAL PROPERTY FTXUI::EXAMPLES)
foreach(file
"index.html"
"index.mjs"
"index.css"
"sw.js"
"run_webassembly.py")
configure_file(${file} ${file})

View File

@@ -18,6 +18,7 @@ example(focus_cursor)
example(gallery)
example(homescreen)
example(input)
example(input_in_frame)
example(input_style)
example(linear_gradient_gallery)
example(maybe)
@@ -38,6 +39,8 @@ example(radiobox)
example(radiobox_in_frame)
example(renderer)
example(resizable_split)
example(scrollbar)
example(selection)
example(slider)
example(slider_direction)
example(slider_rgb)

View File

@@ -1,66 +1,9 @@
// Copyright 2020 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <memory> // for shared_ptr, __shared_ptr_access
#include <string> // for operator+, to_string
#include "ftxui/component/component.hpp"
#include "ftxui/component/screen_interactive.hpp"
#include "ftxui/component/captured_mouse.hpp" // for ftxui
#include "ftxui/component/component.hpp" // for Button, Horizontal, Renderer
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive
#include "ftxui/dom/elements.hpp" // for separator, gauge, text, Element, operator|, vbox, border
using namespace ftxui;
// This is a helper function to create a button with a custom style.
// The style is defined by a lambda function that takes an EntryState and
// returns an Element.
// We are using `center` to center the text inside the button, then `border` to
// add a border around the button, and finally `flex` to make the button fill
// the available space.
ButtonOption Style() {
auto option = ButtonOption::Animated();
option.transform = [](const EntryState& s) {
auto element = text(s.label);
if (s.focused) {
element |= bold;
}
return element | center | borderEmpty | flex;
};
return option;
}
int main() {
int value = 50;
// clang-format off
auto btn_dec_01 = Button("-1", [&] { value += 1; }, Style());
auto btn_inc_01 = Button("+1", [&] { value -= 1; }, Style());
auto btn_dec_10 = Button("-10", [&] { value -= 10; }, Style());
auto btn_inc_10 = Button("+10", [&] { value += 10; }, Style());
// clang-format on
// The tree of components. This defines how to navigate using the keyboard.
// The selected `row` is shared to get a grid layout.
int row = 0;
auto buttons = Container::Vertical({
Container::Horizontal({btn_dec_01, btn_inc_01}, &row) | flex,
Container::Horizontal({btn_dec_10, btn_inc_10}, &row) | flex,
});
// Modify the way to render them on screen:
auto component = Renderer(buttons, [&] {
return vbox({
text("value = " + std::to_string(value)),
separator(),
gauge(value * 0.01f),
separator(),
buttons->Render(),
}) |
border;
});
auto screen = ScreenInteractive::FitComponent();
screen.Loop(component);
return 0;
int main(){
auto screen = ftxui::ScreenInteractive::Fullscreen();
auto testComponent = ftxui::Renderer([](){return ftxui::text("test Component");});
screen.Loop(testComponent);
return 0;
}

View File

@@ -1,30 +1,38 @@
// 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.
#include <memory> // for allocator, __shared_ptr_access
#include <string> // for string, basic_string, operator+, to_string
#include <vector> // for vector
#include <array> // for array
#include <iostream>
#include <memory> // for shared_ptr, __shared_ptr_access
#include <string> // for operator+, to_string
#include "ftxui/component/captured_mouse.hpp" // for ftxui
#include "ftxui/component/component.hpp" // for Input, Renderer, Vertical
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/screen_interactive.hpp" // for Component, ScreenInteractive
#include "ftxui/component/component.hpp" // for Checkbox, Renderer, Vertical
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive
#include "ftxui/dom/elements.hpp" // for operator|, Element, size, border, frame, vscroll_indicator, HEIGHT, LESS_THAN
using namespace ftxui;
int main() {
using namespace ftxui;
bool download = false;
bool upload = false;
bool ping = false;
Component input_list = Container::Vertical({});
std::vector<std::string> items(100, "");
for (size_t i = 0; i < items.size(); ++i) {
input_list->Add(Input(&(items[i]), "placeholder " + std::to_string(i)));
}
auto renderer = Renderer(input_list, [&] {
return input_list->Render() | vscroll_indicator | frame | border |
size(HEIGHT, LESS_THAN, 10);
auto container = Container::Vertical({
Checkbox("Download", &download),
Checkbox("Upload", &upload),
Checkbox("Ping", &ping),
});
auto screen = ScreenInteractive::TerminalOutput();
screen.Loop(renderer);
auto screen = ScreenInteractive::FitComponent();
screen.Loop(container);
std::cout << "---" << std::endl;
std::cout << "Download: " << download << std::endl;
std::cout << "Upload: " << upload << std::endl;
std::cout << "Ping: " << ping << std::endl;
std::cout << "---" << std::endl;
return 0;
}

View File

@@ -97,7 +97,25 @@ int main() {
});
sliders = Wrap("Slider", sliders);
// -- Layout -----------------------------------------------------------------
// A large text:
auto lorel_ipsum = Renderer([] {
return vbox({
text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. "),
text("Sed do eiusmod tempor incididunt ut labore et dolore magna "
"aliqua. "),
text("Ut enim ad minim veniam, quis nostrud exercitation ullamco "
"laboris nisi ut aliquip ex ea commodo consequat. "),
text("Duis aute irure dolor in reprehenderit in voluptate velit esse "
"cillum dolore eu fugiat nulla pariatur. "),
text("Excepteur sint occaecat cupidatat non proident, sunt in culpa "
"qui officia deserunt mollit anim id est laborum. "),
});
});
lorel_ipsum = Wrap("Lorel Ipsum", lorel_ipsum);
// -- Layout
// -----------------------------------------------------------------
auto layout = Container::Vertical({
menu,
toggle,
@@ -106,6 +124,7 @@ int main() {
input,
sliders,
button,
lorel_ipsum,
});
auto component = Renderer(layout, [&] {
@@ -123,6 +142,8 @@ int main() {
sliders->Render(),
separator(),
button->Render(),
separator(),
lorel_ipsum->Render(),
}) |
xflex | size(WIDTH, GREATER_THAN, 40) | border;
});

View File

@@ -424,7 +424,7 @@ int main() {
auto paragraph_renderer_left = Renderer([&] {
std::string str =
"Lorem Ipsum is simply dummy text of the printing and typesetting "
"industry. Lorem Ipsum has been the industry's standard dummy text "
"industry.\nLorem Ipsum has been the industry's standard dummy text "
"ever since the 1500s, when an unknown printer took a galley of type "
"and scrambled it to make a type specimen book.";
return vbox({
@@ -504,7 +504,10 @@ int main() {
auto main_renderer = Renderer(main_container, [&] {
return vbox({
text("FTXUI Demo") | bold | hcenter,
tab_selection->Render(),
hbox({
tab_selection->Render() | flex,
exit_button->Render(),
}),
tab_content->Render() | flex,
});
});

View File

@@ -15,30 +15,50 @@
int main() {
using namespace ftxui;
// The data:
std::string first_name;
std::string last_name;
std::string password;
std::string phoneNumber;
// The basic input components:
Component input_first_name = Input(&first_name, "first name");
Component input_last_name = Input(&last_name, "last name");
// The password input component:
InputOption password_option;
password_option.password = true;
Component input_password = Input(&password, "password", password_option);
// The phone number input component:
// We are using `CatchEvent` to filter out non-digit characters.
Component input_phone_number = Input(&phoneNumber, "phone number");
input_phone_number |= CatchEvent([&](Event event) {
return event.is_character() && !std::isdigit(event.character()[0]);
});
input_phone_number |= CatchEvent([&](Event event) {
return event.is_character() && phoneNumber.size() > 10;
});
// The component tree:
auto component = Container::Vertical({
input_first_name,
input_last_name,
input_password,
input_phone_number,
});
// Tweak how the component tree is rendered:
auto renderer = Renderer(component, [&] {
return vbox({
text("Hello " + first_name + " " + last_name),
separator(),
hbox(text(" First name : "), input_first_name->Render()),
hbox(text(" Last name : "), input_last_name->Render()),
hbox(text(" Password : "), input_password->Render()),
hbox(text(" Phone num : "), input_phone_number->Render()),
separator(),
text("Hello " + first_name + " " + last_name),
text("Your password is " + password),
text("Your phone number is " + phoneNumber),
}) |
border;
});

View File

@@ -0,0 +1,30 @@
// 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.
#include <memory> // for allocator, __shared_ptr_access
#include <string> // for string, basic_string, operator+, to_string
#include <vector> // for vector
#include "ftxui/component/captured_mouse.hpp" // for ftxui
#include "ftxui/component/component.hpp" // for Input, Renderer, Vertical
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/screen_interactive.hpp" // for Component, ScreenInteractive
#include "ftxui/dom/elements.hpp" // for operator|, Element, size, border, frame, vscroll_indicator, HEIGHT, LESS_THAN
int main() {
using namespace ftxui;
Component input_list = Container::Vertical({});
std::vector<std::string> items(100, "");
for (size_t i = 0; i < items.size(); ++i) {
input_list->Add(Input(&(items[i]), "placeholder " + std::to_string(i)));
}
auto renderer = Renderer(input_list, [&] {
return input_list->Render() | vscroll_indicator | frame | border |
size(HEIGHT, LESS_THAN, 10);
});
auto screen = ScreenInteractive::TerminalOutput();
screen.Loop(renderer);
}

View File

@@ -0,0 +1,112 @@
// Copyright 2023 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>
using namespace ftxui;
Component DummyWindowContent() {
class Impl : public ComponentBase {
private:
float scroll_x = 0.1;
float scroll_y = 0.1;
public:
Impl() {
auto content = Renderer([=] {
const std::string lorem =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed "
"do eiusmod tempor incididunt ut labore et dolore magna "
"aliqua. Ut enim ad minim veniam, quis nostrud exercitation "
"ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis "
"aute irure dolor in reprehenderit in voluptate velit esse "
"cillum dolore eu fugiat nulla pariatur. Excepteur sint "
"occaecat cupidatat non proident, sunt in culpa qui officia "
"deserunt mollit anim id est laborum.";
return vbox({
text(lorem.substr(0, -1)), text(lorem.substr(5, -1)), text(""),
text(lorem.substr(10, -1)), text(lorem.substr(15, -1)), text(""),
text(lorem.substr(20, -1)), text(lorem.substr(25, -1)), text(""),
text(lorem.substr(30, -1)), text(lorem.substr(35, -1)), text(""),
text(lorem.substr(40, -1)), text(lorem.substr(45, -1)), text(""),
text(lorem.substr(50, -1)), text(lorem.substr(55, -1)), text(""),
text(lorem.substr(60, -1)), text(lorem.substr(65, -1)), text(""),
text(lorem.substr(70, -1)), text(lorem.substr(75, -1)), text(""),
text(lorem.substr(80, -1)), text(lorem.substr(85, -1)), text(""),
text(lorem.substr(90, -1)), text(lorem.substr(95, -1)), text(""),
text(lorem.substr(100, -1)), text(lorem.substr(105, -1)), text(""),
text(lorem.substr(110, -1)), text(lorem.substr(115, -1)), text(""),
text(lorem.substr(120, -1)), text(lorem.substr(125, -1)), text(""),
text(lorem.substr(130, -1)), text(lorem.substr(135, -1)), text(""),
text(lorem.substr(140, -1)),
});
});
auto scrollable_content = Renderer(content, [&, content] {
return content->Render() | focusPositionRelative(scroll_x, scroll_y) |
frame | flex;
});
SliderOption<float> option_x;
option_x.value = &scroll_x;
option_x.min = 0.f;
option_x.max = 1.f;
option_x.increment = 0.1f;
option_x.direction = Direction::Right;
option_x.color_active = Color::Blue;
option_x.color_inactive = Color::BlueLight;
auto scrollbar_x = Slider(option_x);
SliderOption<float> option_y;
option_y.value = &scroll_y;
option_y.min = 0.f;
option_y.max = 1.f;
option_y.increment = 0.1f;
option_y.direction = Direction::Down;
option_y.color_active = Color::Yellow;
option_y.color_inactive = Color::YellowLight;
auto scrollbar_y = Slider(option_y);
Add(Container::Vertical({
Container::Horizontal({
scrollable_content,
scrollbar_y,
}) | flex,
Container::Horizontal({
scrollbar_x,
Renderer([] { return text(L"x"); }),
}),
}));
}
};
return Make<Impl>();
}
int main() {
auto window_1 = Window({
.inner = DummyWindowContent(),
.title = "First window",
.width = 80,
.height = 30,
});
auto window_2 = Window({
.inner = DummyWindowContent(),
.title = "My window",
.left = 40,
.top = 20,
.width = 80,
.height = 30,
});
auto window_container = Container::Stacked({
window_1,
window_2,
});
auto screen = ScreenInteractive::Fullscreen();
screen.Loop(window_container);
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,87 @@
// Copyright 2020 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <string> // for char_traits, operator+, string, basic_string
#include "ftxui/component/component.hpp" // for Input, Renderer, Vertical
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/component_options.hpp" // for InputOption
#include "ftxui/component/screen_interactive.hpp" // for Component, ScreenInteractive
#include "ftxui/dom/elements.hpp" // for text, hbox, separator, Element, operator|, vbox, border
#include "ftxui/util/ref.hpp" // for Ref
using namespace ftxui;
Element LoremIpsum() {
return vbox({
text("FTXUI: A powerful library for building user interfaces."),
text("Enjoy a rich set of components and a declarative style."),
text("Create beautiful and responsive UIs with minimal effort."),
text("Join the community and experience the power of FTXUI."),
});
}
int main() {
auto screen = ScreenInteractive::TerminalOutput();
auto quit =
Button("Quit", screen.ExitLoopClosure(), ButtonOption::Animated());
int selection_change_counter = 0;
std::string selection_content = "";
screen.SelectionChange([&] {
selection_change_counter++;
selection_content = screen.GetSelection();
});
// The components:
auto renderer = Renderer(quit, [&] {
return vbox({
text("Select changed: " + std::to_string(selection_change_counter) +
" times"),
text("Currently selected: "),
paragraph(selection_content) | vscroll_indicator | frame | border |
size(HEIGHT, EQUAL, 10),
window(text("Horizontal split"), hbox({
LoremIpsum(),
separator(),
LoremIpsum(),
separator(),
LoremIpsum(),
})),
window(text("Vertical split"), vbox({
LoremIpsum(),
separator(),
LoremIpsum(),
separator(),
LoremIpsum(),
})),
window(text("Grid split with different style"),
vbox({
hbox({
LoremIpsum(),
separator(),
LoremIpsum() //
| selectionBackgroundColor(Color::Yellow) //
| selectionColor(Color::Black) //
| selectionStyleReset,
separator(),
LoremIpsum() | selectionColor(Color::Blue),
}),
separator(),
hbox({
LoremIpsum() | selectionColor(Color::Red),
separator(),
LoremIpsum() | selectionStyle([](Pixel& pixel) {
pixel.underlined_double = true;
}),
separator(),
LoremIpsum(),
}),
})),
quit->Render(),
});
});
screen.Loop(renderer);
}

View File

@@ -29,6 +29,7 @@ example(style_dim)
example(style_gallery)
example(style_hyperlink)
example(style_inverted)
example(style_italic)
example(style_strikethrough)
example(style_underlined)
example(style_underlined_double)

View File

@@ -15,6 +15,7 @@ int main() {
hbox({
text("normal") , text(" ") ,
text("bold") | bold , text(" ") ,
text("italic") | italic , text(" ") ,
text("dim") | dim , text(" ") ,
text("inverted") | inverted , text(" ") ,
text("underlined") | underlined , text(" ") ,

View File

@@ -0,0 +1,23 @@
// Copyright 2025 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <ftxui/dom/elements.hpp> // for text, operator|, inverted, Fit, hbox, Element
#include <ftxui/screen/screen.hpp> // for Full, Screen
#include <memory> // for allocator
#include "ftxui/dom/node.hpp" // for Render
#include "ftxui/screen/color.hpp" // for ftxui
int main() {
using namespace ftxui;
auto document = hbox({
text("This text is "),
text("italic") | italic,
text(". Do you like it?"),
});
auto screen = Screen::Create(Dimension::Full(), Dimension::Fit(document));
Render(screen, document);
screen.Print();
return 0;
}

107
examples/index.css Normal file
View File

@@ -0,0 +1,107 @@
@import url(https://fonts.googleapis.com/css?family=Khula:700);
body {
background-color:#EEE;
padding:0px;
margin:0px;
font-family: Khula, Helvetica, sans-serif;
font-size: 130%;
}
.page {
max-width:1300px;
margin: auto;
padding: 10px;
}
a {
box-shadow: inset 0 0 0 0 #54b3d6;
color: #0087b9;
margin: 0 -.25rem;
padding: 0 .25rem;
transition: color .3s ease-in-out,
box-shadow .3s ease-in-out;
}
a:hover {
box-shadow: inset 120px 0 0 0 #54b3d6;
color: white;
}
h1 {
text-decoration: underline;
width:100%;
background-color: rgba(100,100,255,0.5);
padding: 10px;
margin: 0;
}
#selectExample {
flex:1;
}
#selectExample, #selectExample option {
font-size: 16px;
font-family: sans-serif;
font-weight: 700;
line-height: 1.3;
border:0px;
background-color: #bbb;
color:black;
}
#selectExample:focus {
outline:none;
}
#terminal {
width:100%;
height 500px;
height: calc(clamp(200px, 100vh - 300px, 900px));
overflow: hidden;
border:none;
background-color:black;
}
#terminalContainer {
overflow: hidden;
border-radius: 10px;
box-shadow: 0px 2px 10px 0px rgba(0,0,0,0.75),
0px 2px 80px 0px rgba(0,0,0,0.50);
}
.fakeButtons {
height: 10px;
width: 10px;
border-radius: 50%;
border: 1px solid #000;
margin:6px;
background-color: #ff3b47;
border-color: #9d252b;
display: inline-block;
}
.fakeMinimize {
left: 11px;
background-color: #ffc100;
border-color: #9d802c;
}
.fakeZoom {
left: 16px;
background-color: #00d742;
border-color: #049931;
}
.fakeMenu {
display:flex;
flex-direction: row;
width:100%;
box-sizing: border-box;
height: 25px;
background-color: #bbb;
color:black;
margin: 0 auto;
overflow: hidden;
}

View File

@@ -1,174 +1,32 @@
<!DOCTYPE html> <html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FTXUI examples WebAssembly</title>
<script src="https://cdn.jsdelivr.net/npm/xterm@4.18.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-webgl@0.11.4/lib/xterm-addon-webgl.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.min.js"></script>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>➡️</text></svg>">
<link rel="stylesheet" href="index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.11.0/css/xterm.css"></link>
<!--Add COOP/COEP via a ServiceWorker to use SharedArrayBuffer-->
<script>
if ("serviceWorker" in navigator && !window.crossOriginIsolated) {
navigator.serviceWorker.register(new URL("./sw.js", location.href)).then(
registration => {
if (registration.active && !navigator.serviceWorker.controller) {
window.location.reload();
}
},
);
}
</script>
<script type="module" src="index.mjs"></script>
</head>
<body>
<script id="example_script"></script>
<div class="page">
<h1>FTXUI WebAssembly Example </h1>
<p>
<a href="https://github.com/ArthurSonzogni/FTXUI">FTXUI</a> is a simple
functional C++ library for terminal user interface. <br/>
This showcases the: <a href="https://github.com/ArthurSonzogni/FTXUI/tree/master/examples">./example/</a> folder. <br/>
</p>
<p>
On this page, you can try all the examples contained in: <a
href="https://github.com/ArthurSonzogni/FTXUI/tree/master/examples">./example/</a>
Those are compiled using WebAssembly.
</p>
<select id="selectExample"></select>
<div id="terminal"></div>
<div id="terminalContainer">
<div class="fakeMenu">
<div class="fakeButtons fakeClose"></div>
<div class="fakeButtons fakeMinimize"></div>
<div class="fakeButtons fakeZoom"></div>
<select id="selectExample"></select>
</div>
<div id="terminal"></div>
</div>
</div>
</body>
<script>
const example_list = "@EXAMPLES@".split(";");
const url_search_params = new URLSearchParams(window.location.search);
const example = url_search_params.get("file") || "dom/color_gallery";
const select = document.getElementById("selectExample");
for(var i = 0; i < example_list.length; i++) {
var opt = example_list[i];
var el = document.createElement("option");
el.textContent = opt;
el.value = opt;
select.appendChild(el);
}
select.selectedIndex = example_list.findIndex(path => path == example) || 0;
select.addEventListener("change", () => {
location.href = (location.href).split('?')[0] + "?file=" +
example_list[select.selectedIndex];
});
let stdin_buffer = [];
const stdin = () => {
return stdin_buffer.shift() || 0;
}
let stdout_buffer = [];
const stdout = code => {
if (code == 0) {
term.write(new Uint8Array(stdout_buffer));
stdout_buffer = [];
} else {
stdout_buffer.push(code)
}
}
let stderrbuffer = [];
const stderr = code => {
if (code == 0 || code == 10) {
console.error(String.fromCodePoint(...stderrbuffer));
stderrbuffer = [];
} else {
stderrbuffer.push(code)
}
}
const term = new Terminal();
const term_element = document.querySelector('#terminal');
term.open(term_element);
const webgl_addon = new (WebglAddon.WebglAddon)();
term.loadAddon(webgl_addon);
const onBinary = e => {
for(c of e)
stdin_buffer.push(c.charCodeAt(0));
}
term.onBinary(onBinary);
term.onData(onBinary)
term.resize(140,43);
window.Module = {
preRun: () => {
FS.init(stdin, stdout, stderr);
},
postRun: [],
onRuntimeInitialized: () => {
if (window.Module._ftxui_on_resize == undefined)
return;
const fit_addon = new (FitAddon.FitAddon)();
term.loadAddon(fit_addon);
fit_addon.fit();
const resize_handler = () => {
const {cols, rows} = fit_addon.proposeDimensions();
term.resize(cols, rows);
window.Module._ftxui_on_resize(cols, rows);
};
const resize_observer = new ResizeObserver(resize_handler);
resize_observer.observe(term_element);
resize_handler();
// Disable scrollbar
term.write('\x1b[?47h')
},
};
const words = example.split('/')
words[1] = "ftxui_example_" + words[1] + ".js"
document.querySelector("#example_script").src = words.join('/');
</script>
<style>
body {
background-color:#EEE;
padding:20px;
font-family: Helvetica, sans-serif;
font-size: 130%;
}
.page {
max-width:1300px;
margin: auto;
}
h1 {
text-decoration: underline;
}
select {
display:block;
padding: .6em 1.4em .5em .8em;
border-radius: 20px 20px 0px 0px;
font-size: 16px;
font-family: sans-serif;
font-weight: 700;
color: #444;
line-height: 1.3;
background-color:black;
border:0px;
color:white;
transition: color 0.2s linear;
transition: background-color 0.2s linear;
}
#terminal {
width:100%;
height: 500px;
height: calc(clamp(200px, 100vh - 300px, 900px));
overflow: hidden;
border:none;
padding:auto;
}
</style>
</html>

100
examples/index.mjs Normal file
View File

@@ -0,0 +1,100 @@
import xterm from 'https://cdn.jsdelivr.net/npm/xterm@4.18.0/+esm'
import xterm_addon_webgl from 'https://cdn.jsdelivr.net/npm/xterm-addon-webgl@0.11.4/+esm'
import xterm_addon_fit from 'https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/+esm'
// Add COOP/COEP via a ServiceWorker to use SharedArrayBuffer
if ("serviceWorker" in navigator && !window.crossOriginIsolated) {
const url_sw = new URL("./sw.js", location.href);
const registration = await navigator.serviceWorker.register(url_sw);
window.location.reload(); // Reload to ensure the COOP/COEP headers are set.
}
const example_list = "@EXAMPLES@".split(";");
const url_search_params = new URLSearchParams(window.location.search);
const select = document.getElementById("selectExample");
for(const example of example_list) {
const option = document.createElement("option");
option.textContent = example;
option.value = example;
select.appendChild(option);
}
const example = url_search_params.get("file") || "dom/color_gallery";
select.selectedIndex = example_list.findIndex(path => path == example) || 0;
select.addEventListener("change", () => {
history.pushState({}, "", "?file=" + example_list[select.selectedIndex]);
location.reload();
});
const term_element = document.querySelector('#terminal');
const term = new xterm.Terminal();
term.options.scrollback = 0;
term.open(term_element);
const fit_addon = new xterm_addon_fit.FitAddon();
const webgl_addon = new xterm_addon_webgl.WebglAddon();
term.loadAddon(webgl_addon);
term.loadAddon(fit_addon);
const stdin_buffer = [];
const stdout_buffer = [];
const stderr_buffer = [];
const stdin = () => {
return stdin_buffer.shift() || 0;
}
const stdout = code => {
if (code == 0) {
term.write(new Uint8Array(stdout_buffer));
stdout_buffer.length = 0;
} else {
stdout_buffer.push(code)
}
}
const stderr = code => {
if (code == 0 || code == 10) {
console.error(String.fromCodePoint(...stderr_buffer));
stderr_buffer = [];
} else {
stderr_buffer.push(code)
}
}
const onBinary = e => {
for(const c of e)
stdin_buffer.push(c.charCodeAt(0));
}
term.onBinary(onBinary);
term.onData(onBinary)
term.resize(140,43);
window.Module = {
preRun: () => {
FS.init(stdin, stdout, stderr);
},
postRun: [],
onRuntimeInitialized: () => {
if (window.Module._ftxui_on_resize == undefined)
return;
fit_addon.fit();
const resize_handler = () => {
const {cols, rows} = fit_addon.proposeDimensions();
term.resize(cols, rows);
window.Module._ftxui_on_resize(cols, rows);
fit_addon.fit();
};
const resize_observer = new ResizeObserver(resize_handler);
resize_observer.observe(term_element);
resize_handler();
// Disable scrollbar
//term.write('\x1b[?47h')
},
};
const words = example.split('/')
words[1] = "ftxui_example_" + words[1] + ".js"
document.querySelector("#example_script").src = words.join('/');

30
flake.lock generated
View File

@@ -1,12 +1,15 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1678901627,
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
@@ -17,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1679734080,
"narHash": "sha256-z846xfGLlon6t9lqUzlNtBOmsgQLQIZvR6Lt2dImk1M=",
"lastModified": 1697915759,
"narHash": "sha256-WyMj5jGcecD+KC8gEs+wFth1J1wjisZf8kVZH13f1Zo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "dbf5322e93bcc6cfc52268367a8ad21c09d76fea",
"rev": "51d906d2341c9e866e48c2efcaac0f2d70bfd43e",
"type": "github"
},
"original": {
@@ -36,6 +39,21 @@
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -8,9 +8,11 @@
outputs = {self, nixpkgs, flake-utils}:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs { inherit system; }; in
{
packages.ftxui = pkgs.stdenv.mkDerivation rec {
let pkgs = import nixpkgs { inherit system; }; in
let llvm = pkgs.llvmPackages_latest; in
{
packages = rec {
default = pkgs.stdenv.mkDerivation rec {
pname = "ftxui";
version = "v4.0.0";
src = pkgs.fetchFromGitHub {
@@ -56,6 +58,19 @@
platforms = pkgs.lib.platforms.all;
};
};
}
);
ftxui = default;
};
devShells = {
default = pkgs.mkShell {
nativeBuildInputs = [
pkgs.cmake
pkgs.clang-tools
llvm.clang
];
};
};
}
);
}

View File

@@ -44,12 +44,16 @@ class ComponentBase {
ComponentBase* Parent() const;
Component& ChildAt(size_t i);
size_t ChildCount() const;
int Index() const;
void Add(Component children);
void Detach();
void DetachAllChildren();
// Renders the component.
virtual Element Render();
Element Render();
// Override this function modify how `Render` works.
virtual Element OnRender();
// Handles an event.
// By default, reduce on children with a lazy OR.
@@ -93,6 +97,7 @@ class ComponentBase {
private:
ComponentBase* parent_ = nullptr;
bool in_render = false;
};
} // namespace ftxui

View File

@@ -25,6 +25,7 @@ struct EntryState {
bool state; ///< The state of the button/checkbox/radiobox
bool active; ///< Whether the entry is the active one.
bool focused; ///< Whether the entry is one focused by the user.
int index; ///< Index of the entry when applicable or -1.
};
struct UnderlineOption {

View File

@@ -16,6 +16,7 @@
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
#include "ftxui/component/event.hpp" // for Event
#include "ftxui/component/task.hpp" // for Task, Closure
#include "ftxui/dom/selection.hpp" // for SelectionOption
#include "ftxui/screen/screen.hpp" // for Screen
namespace ftxui {
@@ -68,6 +69,10 @@ class ScreenInteractive : public Screen {
void ForceHandleCtrlC(bool force);
void ForceHandleCtrlZ(bool force);
// Selection API.
std::string GetSelection();
void SelectionChange(std::function<void()> callback);
private:
void ExitNow();
@@ -82,6 +87,8 @@ class ScreenInteractive : public Screen {
void RunOnceBlocking(Component component);
void HandleTask(Component component, Task& task);
bool HandleSelection(bool handled, Event event);
void RefreshSelection();
void Draw(Component component);
void ResetCursorPosition();
@@ -129,6 +136,22 @@ class ScreenInteractive : public Screen {
// The style of the cursor to restore on exit.
int cursor_reset_shape_ = 1;
// Selection API:
CapturedMouse selection_pending_;
struct SelectionData {
int start_x = -1;
int start_y = -1;
int end_x = -2;
int end_y = -2;
bool empty = true;
bool operator==(const SelectionData& other) const;
bool operator!=(const SelectionData& other) const;
};
SelectionData selection_data_;
SelectionData selection_data_previous_;
std::unique_ptr<Selection> selection_;
std::function<void()> selection_on_change_;
friend class Loop;
public:

View File

@@ -95,6 +95,7 @@ Element canvas(std::function<void(Canvas&)>);
// -- Decorator ---
Element bold(Element);
Element dim(Element);
Element italic(Element);
Element inverted(Element);
Element underlined(Element);
Element underlinedDouble(Element);
@@ -113,6 +114,11 @@ Decorator focusPositionRelative(float x, float y);
Element automerge(Element child);
Decorator hyperlink(std::string link);
Element hyperlink(std::string link, Element child);
Element selectionStyleReset(Element);
Decorator selectionColor(Color foreground);
Decorator selectionBackgroundColor(Color foreground);
Decorator selectionForegroundColor(Color foreground);
Decorator selectionStyle(std::function<void(Pixel&)> style);
// --- Layout is
// Horizontal, Vertical or stacked set of elements.
@@ -156,7 +162,7 @@ Element frame(Element);
Element xframe(Element);
Element yframe(Element);
Element focus(Element);
Element select(Element);
Element select(Element e); // Deprecated - Alias for focus.
// --- Cursor ---
// Those are similar to `focus`, but also change the shape of the cursor.

View File

@@ -8,6 +8,7 @@
#include <vector> // for vector
#include "ftxui/dom/requirement.hpp" // for Requirement
#include "ftxui/dom/selection.hpp" // for Selection
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/screen.hpp"
@@ -22,7 +23,7 @@ using Elements = std::vector<Element>;
class Node {
public:
Node();
Node(Elements children);
explicit Node(Elements children);
Node(const Node&) = delete;
Node(const Node&&) = delete;
Node& operator=(const Node&) = delete;
@@ -40,9 +41,15 @@ class Node {
// Propagated from Parents to Children.
virtual void SetBox(Box box);
// Step 3: Draw this element.
// Step 3: (optional) Selection
// Propagated from Parents to Children.
virtual void Select(Selection& selection);
// Step 4: Draw this element.
virtual void Render(Screen& screen);
virtual std::string GetSelectedContent(Selection& selection);
// Layout may not resolve within a single iteration for some elements. This
// allows them to request additionnal iterations. This signal must be
// forwarded to children at least once.
@@ -52,6 +59,8 @@ class Node {
};
virtual void Check(Status* status);
friend void Render(Screen& screen, Node* node, Selection& selection);
protected:
Elements children_;
Requirement requirement_;
@@ -60,6 +69,10 @@ class Node {
void Render(Screen& screen, const Element& element);
void Render(Screen& screen, Node* node);
void Render(Screen& screen, Node* node, Selection& selection);
std::string GetNodeSelectedContent(Screen& screen,
Node* node,
Selection& selection);
} // namespace ftxui

View File

@@ -5,8 +5,10 @@
#define FTXUI_DOM_REQUIREMENT_HPP
#include "ftxui/screen/box.hpp"
#include "ftxui/screen/screen.hpp"
namespace ftxui {
class Node;
struct Requirement {
// The required size to fully draw the element.
@@ -20,13 +22,28 @@ struct Requirement {
int flex_shrink_y = 0;
// Focus management to support the frame/focus/select element.
enum Selection {
NORMAL = 0,
SELECTED = 1,
FOCUSED = 2,
struct Focused {
bool enabled = false;
Box box;
Node* node = nullptr;
Screen::Cursor::Shape cursor_shape = Screen::Cursor::Shape::Hidden;
// Internal for interactions with components.
bool component_active = false;
// Return whether this requirement should be preferred over the other.
bool Prefer(const Focused& other) const {
if (!other.enabled) {
return false;
}
if (!enabled) {
return true;
}
return other.component_active && !component_active;
}
};
Selection selection = NORMAL;
Box selected_box;
Focused focused;
};
} // namespace ftxui

View File

@@ -0,0 +1,50 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef FTXUI_DOM_SELECTION_HPP
#define FTXUI_DOM_SELECTION_HPP
#include <functional>
#include <sstream>
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/pixel.hpp" // for Pixel
namespace ftxui {
/// @brief Represent a selection in the terminal.
class Selection {
public:
Selection(); // Empty selection.
Selection(int start_x, int start_y, int end_x, int end_y);
const Box& GetBox() const;
Selection SaturateHorizontal(Box box);
Selection SaturateVertical(Box box);
bool IsEmpty() const { return empty_; }
void AddPart(const std::string& part, int y, int left, int right);
std::string GetParts() { return parts_.str(); }
private:
Selection(int start_x, int start_y, int end_x, int end_y, Selection* parent);
const int start_x_ = 0;
const int start_y_ = 0;
const int end_x_ = 0;
const int end_y_ = 0;
const Box box_ = {};
Selection* const parent_ = this;
const bool empty_ = true;
std::stringstream parts_;
// The position of the last inserted part.
int x_ = 0;
int y_ = 0;
};
} // namespace ftxui
#endif /* end of include guard: FTXUI_DOM_SELECTION_HPP */

View File

@@ -36,8 +36,8 @@ class TableSelection;
class Table {
public:
Table();
Table(std::vector<std::vector<std::string>>);
Table(std::vector<std::vector<Element>>);
explicit Table(std::vector<std::vector<std::string>>);
explicit Table(std::vector<std::vector<Element>>);
Table(std::initializer_list<std::vector<std::string>> init);
TableSelection SelectAll();
TableSelection SelectCell(int column, int row);

View File

@@ -14,6 +14,7 @@ struct Box {
static auto Intersection(Box a, Box b) -> Box;
static auto Union(Box a, Box b) -> Box;
void Shift(int x, int y);
bool Contain(int x, int y) const;
bool IsEmpty() const;
bool operator==(const Box& other) const;

View File

@@ -17,6 +17,7 @@ struct Pixel {
: blink(false),
bold(false),
dim(false),
italic(false),
inverted(false),
underlined(false),
underlined_double(false),
@@ -27,6 +28,7 @@ struct Pixel {
bool blink : 1;
bool bold : 1;
bool dim : 1;
bool italic : 1;
bool inverted : 1;
bool underlined : 1;
bool underlined_double : 1;

View File

@@ -4,12 +4,14 @@
#ifndef FTXUI_SCREEN_SCREEN_HPP
#define FTXUI_SCREEN_SCREEN_HPP
#include <cstdint> // for uint8_t
#include <string> // for string, basic_string, allocator
#include <vector> // for vector
#include <cstdint> // for uint8_t
#include <functional> // for function
#include <string> // for string, basic_string, allocator
#include <vector> // for vector
#include "ftxui/screen/image.hpp" // for Pixel, Image
#include "ftxui/screen/terminal.hpp" // for Dimensions
#include "ftxui/util/autoreset.hpp" // for AutoReset
namespace ftxui {
@@ -67,9 +69,18 @@ class Screen : public Image {
uint8_t RegisterHyperlink(const std::string& link);
const std::string& Hyperlink(uint8_t id) const;
using SelectionStyle = std::function<void(Pixel&)>;
const SelectionStyle& GetSelectionStyle() const;
void SetSelectionStyle(SelectionStyle decorator);
protected:
Cursor cursor_;
std::vector<std::string> hyperlinks_ = {""};
// The current selection style. This is overridden by various dom elements.
SelectionStyle selection_style_ = [](Pixel& pixel) {
pixel.inverted ^= true;
};
};
} // namespace ftxui

View File

@@ -37,7 +37,7 @@ class ButtonBase : public ComponentBase, public ButtonOption {
explicit ButtonBase(ButtonOption option) : ButtonOption(std::move(option)) {}
// Component implementation:
Element Render() override {
Element OnRender() override {
const bool active = Active();
const bool focused = Focused();
const bool focused_or_hover = focused || mouse_hover_;
@@ -47,17 +47,16 @@ class ButtonBase : public ComponentBase, public ButtonOption {
SetAnimationTarget(target);
}
auto focus_management = focused ? focus : active ? select : nothing;
const EntryState state = {
*label,
false,
active,
focused_or_hover,
const EntryState state{
*label, false, active, focused_or_hover, Index(),
};
auto element = (transform ? transform : DefaultTransform) //
(state);
return element | AnimatedColorStyle() | focus_management | reflect(box_);
element |= AnimatedColorStyle();
element |= focus;
element |= reflect(box_);
return element;
}
Decorator AnimatedColorStyle() {

View File

@@ -23,19 +23,17 @@ class CheckboxBase : public ComponentBase, public CheckboxOption {
private:
// Component implementation.
Element Render() override {
Element OnRender() override {
const bool is_focused = Focused();
const bool is_active = Active();
auto focus_management = is_focused ? focus : is_active ? select : nothing;
auto entry_state = EntryState{
*label,
*checked,
is_active,
is_focused || hovered_,
*label, *checked, is_active, is_focused || hovered_, -1,
};
auto element = (transform ? transform : CheckboxOption::Simple().transform)(
entry_state);
return element | focus_management | reflect(box_);
element |= focus;
element |= reflect(box_);
return element;
}
bool OnEvent(Event event) override {
@@ -72,7 +70,6 @@ class CheckboxBase : public ComponentBase, public CheckboxOption {
event.mouse().motion == Mouse::Pressed) {
*checked = !*checked;
on_change();
TakeFocus();
return true;
}

View File

@@ -15,6 +15,8 @@
#include "ftxui/component/event.hpp" // for Event
#include "ftxui/component/screen_interactive.hpp" // for Component, ScreenInteractive
#include "ftxui/dom/elements.hpp" // for text, Element
#include "ftxui/dom/node.hpp" // for Node, Elements
#include "ftxui/screen/box.hpp" // for Box
namespace ftxui::animation {
class Params;
@@ -51,6 +53,22 @@ size_t ComponentBase::ChildCount() const {
return children_.size();
}
/// @brief Return index of the component in its parent. -1 if no parent.
/// @ingroup component
int ComponentBase::Index() const {
if (parent_ == nullptr) {
return -1;
}
int index = 0;
for (const Component& child : parent_->children_) {
if (child.get() == this) {
return index;
}
index++;
}
return -1; // Not reached.
}
/// @brief Add a child.
/// @@param child The child to be attached.
/// @ingroup component
@@ -87,10 +105,46 @@ void ComponentBase::DetachAllChildren() {
}
/// @brief Draw the component.
/// Build a ftxui::Element to be drawn on the ftxi::Screen representing this
/// ftxui::ComponentBase.
/// Build a ftxui::Element to be drawn on the ftxui::Screen representing this
/// ftxui::ComponentBase. Please override OnRender() to modify the rendering.
/// @ingroup component
Element ComponentBase::Render() {
// Some users might call `ComponentBase::Render()` from
// `T::OnRender()`. To avoid infinite recursion, we use a flag.
if (in_render) {
return ComponentBase::OnRender();
}
in_render = true;
Element element = OnRender();
in_render = false;
class Wrapper : public Node {
public:
bool active_ = false;
Wrapper(Element child, bool active)
: Node({std::move(child)}), active_(active) {}
void SetBox(Box box) override {
Node::SetBox(box);
children_[0]->SetBox(box);
}
void ComputeRequirement() override {
Node::ComputeRequirement();
requirement_.focused.component_active = active_;
}
};
return std::make_shared<Wrapper>(std::move(element), Active());
}
/// @brief Draw the component.
/// Build a ftxui::Element to be drawn on the ftxi::Screen representing this
/// ftxui::ComponentBase. This function is means to be overridden.
/// @ingroup component
Element ComponentBase::OnRender() {
if (children_.size() == 1) {
return children_.front()->Render();
}

View File

@@ -98,7 +98,7 @@ class VerticalContainer : public ContainerBase {
public:
using ContainerBase::ContainerBase;
Element Render() override {
Element OnRender() override {
Elements elements;
elements.reserve(children_.size());
for (auto& it : children_) {
@@ -163,6 +163,7 @@ class VerticalContainer : public ContainerBase {
return false;
}
const int old_selected = *selector_;
if (event.mouse().button == Mouse::WheelUp) {
MoveSelector(-1);
}
@@ -171,7 +172,7 @@ class VerticalContainer : public ContainerBase {
}
*selector_ = std::max(0, std::min(int(children_.size()) - 1, *selector_));
return true;
return old_selected != *selector_;
}
Box box_;
@@ -181,7 +182,7 @@ class HorizontalContainer : public ContainerBase {
public:
using ContainerBase::ContainerBase;
Element Render() override {
Element OnRender() override {
Elements elements;
elements.reserve(children_.size());
for (auto& it : children_) {
@@ -217,7 +218,7 @@ class TabContainer : public ContainerBase {
public:
using ContainerBase::ContainerBase;
Element Render() override {
Element OnRender() override {
const Component active_child = ActiveChild();
if (active_child) {
return active_child->Render();
@@ -243,7 +244,7 @@ class StackedContainer : public ContainerBase {
: ContainerBase(std::move(children), nullptr) {}
private:
Element Render() final {
Element OnRender() final {
Elements elements;
for (auto& child : children_) {
elements.push_back(child->Render());
@@ -333,7 +334,7 @@ Component Vertical(Components children) {
/// children_2,
/// children_3,
/// children_4,
/// });
/// }, &selected_children);
/// ```
Component Vertical(Components children, int* selector) {
return std::make_shared<VerticalContainer>(std::move(children), selector);
@@ -354,7 +355,7 @@ Component Vertical(Components children, int* selector) {
/// children_2,
/// children_3,
/// children_4,
/// }, &selected_children);
/// });
/// ```
Component Horizontal(Components children) {
return Horizontal(std::move(children), nullptr);

View File

@@ -44,10 +44,14 @@ Component Dropdown(DropdownOption option) {
}));
}
Element Render() override {
radiobox.selected =
Element OnRender() override {
selected_ =
util::clamp(radiobox.selected(), 0, int(radiobox.entries.size()) - 1);
title_ = radiobox.entries[selected_()];
selected_ = util::clamp(selected_(), 0, int(radiobox.entries.size()) - 1);
if (selected_() >= 0 && selected_() < int(radiobox.entries.size())) {
title_ = radiobox.entries[selected_()];
}
return transform(*open_, checkbox_->Render(), radiobox_->Render());
}
@@ -66,10 +70,13 @@ Component Dropdown(DropdownOption option) {
// Auto-close the dropdown when the user selects an item, even if the item
// it the same as the previous one.
if (open_old && open_()) {
const bool should_close = (selected_() != selected_old) || //
(event == Event::Return) || //
(event == Event::Character(' ')) || //
(event == Event::Escape); //
const bool should_close =
(selected_() != selected_old) || //
(event == Event::Return) || //
(event == Event::Character(' ')) || //
(event == Event::Escape) || //
(event.is_mouse() && event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Pressed);
if (should_close) {
checkbox_->TakeFocus();

View File

@@ -0,0 +1,34 @@
// Copyright 2025 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "ftxui/component/component.hpp" // for Horizontal, Vertical, Button, Tab
#include "ftxui/component/component_base.hpp" // for ComponentBase, Component
#include "ftxui/component/event.hpp" // for Event, Event::Tab, Event::TabReverse, Event::ArrowDown, Event::ArrowLeft, Event::ArrowRight, Event::ArrowUp
#include "gtest/gtest.h" // for AssertionResult, Message, TestPartResult, EXPECT_EQ, EXPECT_FALSE, Test, EXPECT_TRUE, TEST
namespace ftxui {
TEST(DropdownTest, Empty) {
std::vector<std::string> list = {};
int index = 0;
auto dropdown = Dropdown(list, &index);
dropdown->OnEvent(Event::Return);
auto screen = Screen(8, 8);
auto document = dropdown->Render();
Render(screen, document);
EXPECT_EQ(screen.ToString(),
"╭──────╮\r\n"
"│↓ │\r\n"
"├──────┤\r\n"
"│ │\r\n"
"│ │\r\n"
"│ │\r\n"
"│ │\r\n"
"╰──────╯");
}
} // namespace ftxui

View File

@@ -49,8 +49,8 @@ Component Hoverable(Component component, bool* hover) {
}
private:
Element Render() override {
return ComponentBase::Render() | reflect(box_);
Element OnRender() override {
return ComponentBase::OnRender() | reflect(box_);
}
bool OnEvent(Event event) override {
@@ -98,8 +98,8 @@ Component Hoverable(Component component,
}
private:
Element Render() override {
return ComponentBase::Render() | reflect(box_);
Element OnRender() override {
return ComponentBase::OnRender() | reflect(box_);
}
bool OnEvent(Event event) override {

View File

@@ -96,9 +96,9 @@ class InputBase : public ComponentBase, public InputOption {
private:
// Component implementation:
Element Render() override {
Element OnRender() override {
const bool is_focused = Focused();
const auto focused = (!is_focused && !hovered_) ? select
const auto focused = (!is_focused && !hovered_) ? nothing
: insert() ? focusCursorBarBlinking
: focusCursorBlockBlinking;
@@ -108,15 +108,12 @@ class InputBase : public ComponentBase, public InputOption {
// placeholder.
if (content->empty()) {
auto element = text(placeholder()) | xflex | frame;
if (is_focused) {
element |= focus;
}
return transform_func({
std::move(element), hovered_, is_focused,
true // placeholder
}) |
reflect(box_);
focus | reflect(box_);
}
Elements elements;
@@ -176,7 +173,7 @@ class InputBase : public ComponentBase, public InputOption {
elements.push_back(element);
}
auto element = vbox(std::move(elements)) | frame;
auto element = vbox(std::move(elements), cursor_line) | frame;
return transform_func({
std::move(element), hovered_, is_focused,
false // placeholder

View File

@@ -24,8 +24,8 @@ Component Maybe(Component child, std::function<bool()> show) {
explicit Impl(std::function<bool()> show) : show_(std::move(show)) {}
private:
Element Render() override {
return show_() ? ComponentBase::Render() : std::make_unique<Node>();
Element OnRender() override {
return show_() ? ComponentBase::OnRender() : std::make_unique<Node>();
}
bool Focusable() const override {
return show_() && ComponentBase::Focusable();

View File

@@ -105,7 +105,7 @@ class MenuBase : public ComponentBase, public MenuOption {
}
}
Element Render() override {
Element OnRender() override {
Clamp();
UpdateAnimationTarget();
@@ -123,22 +123,18 @@ class MenuBase : public ComponentBase, public MenuOption {
const bool is_selected = (selected() == i);
const EntryState state = {
entries[i],
false,
is_selected,
is_focused,
entries[i], false, is_selected, is_focused, i,
};
auto focus_management = (selected_focus_ != i) ? nothing
: is_menu_focused ? focus
: select;
const Element element =
(entries_option.transform ? entries_option.transform
: DefaultOptionTransform) //
Element element = (entries_option.transform ? entries_option.transform
: DefaultOptionTransform) //
(state);
elements.push_back(element | AnimatedColorStyle(i) | reflect(boxes_[i]) |
focus_management);
if (selected_focus_ == i) {
element |= focus;
}
element |= AnimatedColorStyle(i);
element |= reflect(boxes_[i]);
elements.push_back(element);
}
if (elements_postfix) {
elements.push_back(elements_postfix());
@@ -148,8 +144,9 @@ class MenuBase : public ComponentBase, public MenuOption {
std::reverse(elements.begin(), elements.end());
}
const Element bar =
IsHorizontal() ? hbox(std::move(elements)) : vbox(std::move(elements));
const Element bar = IsHorizontal()
? hbox(std::move(elements), selected_focus_)
: vbox(std::move(elements), selected_focus_);
if (!underline.enabled) {
return bar | reflect(box_);
@@ -621,23 +618,22 @@ Component MenuEntry(MenuEntryOption option) {
: MenuEntryOption(std::move(option)) {}
private:
Element Render() override {
const bool focused = Focused();
Element OnRender() override {
const bool is_focused = Focused();
UpdateAnimationTarget();
const EntryState state = {
label(),
false,
hovered_,
focused,
const EntryState state{
label(), false, hovered_, is_focused, Index(),
};
const Element element =
(transform ? transform : DefaultOptionTransform) //
Element element = (transform ? transform : DefaultOptionTransform) //
(state);
auto focus_management = focused ? select : nothing;
return element | AnimatedColorStyle() | focus_management | reflect(box_);
if (is_focused) {
element |= focus;
}
return element | AnimatedColorStyle() | reflect(box_);
}
void UpdateAnimationTarget() {
@@ -698,7 +694,6 @@ Component MenuEntry(MenuEntryOption option) {
animator_foreground_.OnAnimation(params);
}
MenuEntryOption option_;
Box box_;
bool hovered_ = false;

View File

@@ -226,5 +226,50 @@ TEST(MenuTest, AnimationsVertical) {
}
}
TEST(MenuTest, EntryIndex) {
int selected = 0;
std::vector<std::string> entries = {"0", "1", "2"};
auto option = MenuOption::Vertical();
option.entries = &entries;
option.selected = &selected;
option.entries_option.transform = [&](const EntryState& state) {
int curidx = std::stoi(state.label);
EXPECT_EQ(state.index, curidx);
return text(state.label);
};
auto menu = Menu(option);
menu->OnEvent(Event::ArrowDown);
menu->OnEvent(Event::ArrowDown);
menu->OnEvent(Event::Return);
entries.resize(2);
(void)menu->Render();
}
TEST(MenuTest, MenuEntryIndex) {
int selected = 0;
MenuEntryOption option;
option.transform = [&](const EntryState& state) {
int curidx = std::stoi(state.label);
EXPECT_EQ(state.index, curidx);
return text(state.label);
};
auto menu = Container::Vertical(
{
MenuEntry("0", option),
MenuEntry("1", option),
MenuEntry("2", option),
},
&selected);
menu->OnEvent(Event::ArrowDown);
menu->OnEvent(Event::ArrowDown);
menu->OnEvent(Event::Return);
for (int index = 0; index < menu->ChildCount(); index++) {
EXPECT_EQ(menu->ChildAt(index)->Index(), index);
}
}
} // namespace ftxui
// NOLINTEND

View File

@@ -26,7 +26,7 @@ Component Modal(Component main, Component modal, const bool* show_modal) {
}
private:
Element Render() override {
Element OnRender() override {
selector_ = *show_modal_;
auto document = main_->Render();
if (*show_modal_) {

View File

@@ -28,7 +28,7 @@ class RadioboxBase : public ComponentBase, public RadioboxOption {
: RadioboxOption(option) {}
private:
Element Render() override {
Element OnRender() override {
Clamp();
Elements elements;
const bool is_menu_focused = Focused();
@@ -36,21 +36,17 @@ class RadioboxBase : public ComponentBase, public RadioboxOption {
for (int i = 0; i < size(); ++i) {
const bool is_focused = (focused_entry() == i) && is_menu_focused;
const bool is_selected = (hovered_ == i);
auto focus_management = !is_selected ? nothing
: is_menu_focused ? focus
: select;
auto state = EntryState{
entries[i],
selected() == i,
is_selected,
is_focused,
entries[i], selected() == i, is_selected, is_focused, i,
};
auto element =
(transform ? transform : RadioboxOption::Simple().transform)(state);
elements.push_back(element | focus_management | reflect(boxes_[i]));
if (is_selected) {
element |= focus;
}
elements.push_back(element | reflect(boxes_[i]));
}
return vbox(std::move(elements)) | reflect(box_);
return vbox(std::move(elements), hovered_) | reflect(box_);
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity)

View File

@@ -31,7 +31,7 @@ Component Renderer(std::function<Element()> render) {
public:
explicit Impl(std::function<Element()> render)
: render_(std::move(render)) {}
Element Render() override { return render_(); }
Element OnRender() override { return render_(); }
std::function<Element()> render_;
};
@@ -88,7 +88,7 @@ Component Renderer(std::function<Element(bool)> render) {
: render_(std::move(render)) {}
private:
Element Render() override { return render_(Focused()) | reflect(box_); }
Element OnRender() override { return render_(Focused()) | reflect(box_); }
bool Focusable() const override { return true; }
bool OnEvent(Event event) override {
if (event.is_mouse() && box_.Contain(event.mouse().x, event.mouse().y)) {

View File

@@ -94,7 +94,7 @@ class ResizableSplitBase : public ComponentBase {
return false;
}
Element Render() final {
Element OnRender() final {
switch (options_->direction()) {
case Direction::Left:
return RenderLeft();

View File

@@ -576,6 +576,18 @@ void ScreenInteractive::ForceHandleCtrlZ(bool force) {
force_handle_ctrl_z_ = force;
}
/// @brief Returns the content of the current selection
std::string ScreenInteractive::GetSelection() {
if (!selection_) {
return "";
}
return selection_->GetParts();
}
void ScreenInteractive::SelectionChange(std::function<void()> callback) {
selection_on_change_ = std::move(callback);
}
/// @brief Return the currently active screen, or null if none.
// static
ScreenInteractive* ScreenInteractive::Active() {
@@ -751,6 +763,14 @@ void ScreenInteractive::RunOnce(Component component) {
ExecuteSignalHandlers();
}
Draw(std::move(component));
if (selection_data_previous_ != selection_data_) {
selection_data_previous_ = selection_data_;
if (selection_on_change_) {
selection_on_change_();
Post(Event::Custom);
}
}
}
// private
@@ -781,7 +801,9 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
arg.screen_ = this;
const bool handled = component->OnEvent(arg);
bool handled = component->OnEvent(arg);
handled = HandleSelection(handled, arg);
if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
RecordSignal(SIGABRT);
@@ -824,6 +846,59 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
// clang-format on
}
// private
bool ScreenInteractive::HandleSelection(bool handled, Event event) {
if (handled) {
selection_pending_ = nullptr;
selection_data_.empty = true;
selection_ = nullptr;
return true;
}
if (!event.is_mouse()) {
return false;
}
auto& mouse = event.mouse();
if (mouse.button != Mouse::Left) {
return false;
}
if (mouse.motion == Mouse::Pressed) {
selection_pending_ = CaptureMouse();
selection_data_.start_x = mouse.x;
selection_data_.start_y = mouse.y;
selection_data_.end_x = mouse.x;
selection_data_.end_y = mouse.y;
return false;
}
if (!selection_pending_) {
return false;
}
if (mouse.motion == Mouse::Moved) {
if ((mouse.x != selection_data_.end_x) ||
(mouse.y != selection_data_.end_y)) {
selection_data_.end_x = mouse.x;
selection_data_.end_y = mouse.y;
selection_data_.empty = false;
}
return true;
}
if (mouse.motion == Mouse::Released) {
selection_pending_ = nullptr;
selection_data_.end_x = mouse.x;
selection_data_.end_y = mouse.y;
selection_data_.empty = false;
return true;
}
return false;
}
// private
// NOLINTNEXTLINE
void ScreenInteractive::Draw(Component component) {
@@ -899,7 +974,12 @@ void ScreenInteractive::Draw(Component component) {
#endif
previous_frame_resized_ = resized;
Render(*this, document);
selection_ = selection_data_.empty
? std::make_unique<Selection>()
: std::make_unique<Selection>(
selection_data_.start_x, selection_data_.start_y, //
selection_data_.end_x, selection_data_.end_y);
Render(*this, document.get(), *selection_);
// Set cursor position for user using tools to insert CJK characters.
{
@@ -988,4 +1068,21 @@ void ScreenInteractive::Signal(int signal) {
#endif
}
bool ScreenInteractive::SelectionData::operator==(
const ScreenInteractive::SelectionData& other) const {
if (empty && other.empty) {
return true;
}
if (empty || other.empty) {
return false;
}
return start_x == other.start_x && start_y == other.start_y &&
end_x == other.end_x && end_y == other.end_y;
}
bool ScreenInteractive::SelectionData::operator!=(
const ScreenInteractive::SelectionData& other) const {
return !(*this == other);
}
} // namespace ftxui.

View File

@@ -16,7 +16,6 @@
#include "ftxui/dom/elements.hpp" // for operator|, text, Element, xflex, hbox, color, underlined, reflect, Decorator, dim, vcenter, focus, nothing, select, yflex, gaugeDirection
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/color.hpp" // for Color, Color::GrayDark, Color::White
#include "ftxui/screen/util.hpp" // for clamp
#include "ftxui/util/ref.hpp" // for ConstRef, Ref, ConstStringRef
namespace ftxui {
@@ -39,7 +38,7 @@ class SliderBase : public SliderOption<T>, public ComponentBase {
public:
explicit SliderBase(SliderOption<T> options) : SliderOption<T>(options) {}
Element Render() override {
Element OnRender() override {
auto gauge_color =
Focused() ? color(this->color_active) : color(this->color_inactive);
const float percent =
@@ -134,53 +133,52 @@ class SliderBase : public SliderOption<T>, public ComponentBase {
return ComponentBase::OnEvent(event);
}
bool OnCapturedMouseEvent(Event event) {
if (event.mouse().motion == Mouse::Released) {
captured_mouse_ = nullptr;
return true;
}
T old_value = this->value();
switch (this->direction) {
case Direction::Right: {
this->value() = this->min() + (event.mouse().x - gauge_box_.x_min) *
(this->max() - this->min()) /
(gauge_box_.x_max - gauge_box_.x_min);
break;
}
case Direction::Left: {
this->value() = this->max() - (event.mouse().x - gauge_box_.x_min) *
(this->max() - this->min()) /
(gauge_box_.x_max - gauge_box_.x_min);
break;
}
case Direction::Down: {
this->value() = this->min() + (event.mouse().y - gauge_box_.y_min) *
(this->max() - this->min()) /
(gauge_box_.y_max - gauge_box_.y_min);
break;
}
case Direction::Up: {
this->value() = this->max() - (event.mouse().y - gauge_box_.y_min) *
(this->max() - this->min()) /
(gauge_box_.y_max - gauge_box_.y_min);
break;
}
}
this->value() = std::max(this->min(), std::min(this->max(), this->value()));
if (old_value != this->value() && this->on_change) {
this->on_change();
}
return true;
}
bool OnMouseEvent(Event event) {
if (captured_mouse_) {
if (event.mouse().motion == Mouse::Released) {
captured_mouse_ = nullptr;
return true;
}
T old_value = this->value();
switch (this->direction) {
case Direction::Right: {
this->value() =
this->min() + (event.mouse().x - gauge_box_.x_min) *
(this->max() - this->min()) /
(gauge_box_.x_max - gauge_box_.x_min);
break;
}
case Direction::Left: {
this->value() =
this->max() - (event.mouse().x - gauge_box_.x_min) *
(this->max() - this->min()) /
(gauge_box_.x_max - gauge_box_.x_min);
break;
}
case Direction::Down: {
this->value() =
this->min() + (event.mouse().y - gauge_box_.y_min) *
(this->max() - this->min()) /
(gauge_box_.y_max - gauge_box_.y_min);
break;
}
case Direction::Up: {
this->value() =
this->max() - (event.mouse().y - gauge_box_.y_min) *
(this->max() - this->min()) /
(gauge_box_.y_max - gauge_box_.y_min);
break;
}
}
this->value() =
std::max(this->min(), std::min(this->max(), this->value()));
if (old_value != this->value() && this->on_change) {
this->on_change();
}
return true;
return OnCapturedMouseEvent(event);
}
if (event.mouse().button != Mouse::Left) {
@@ -198,7 +196,7 @@ class SliderBase : public SliderOption<T>, public ComponentBase {
if (captured_mouse_) {
TakeFocus();
return true;
return OnCapturedMouseEvent(event);
}
return false;
@@ -242,19 +240,21 @@ class SliderWithLabel : public ComponentBase {
return true;
}
Element Render() override {
auto focus_management = Focused() ? focus : Active() ? select : nothing;
Element OnRender() override {
auto gauge_color = (Focused() || mouse_hover_) ? color(Color::White)
: color(Color::GrayDark);
return hbox({
text(label_()) | dim | vcenter,
hbox({
text("["),
ComponentBase::Render() | underlined,
text("]"),
}) | xflex,
}) |
gauge_color | xflex | reflect(box_) | focus_management;
auto element = hbox({
text(label_()) | dim | vcenter,
hbox({
text("["),
ComponentBase::Render() | underlined,
text("]"),
}) | xflex,
}) |
gauge_color | xflex | reflect(box_);
element |= focus;
return element;
}
ConstStringRef label_;

View File

@@ -60,17 +60,17 @@ TEST(SliderTest, Right) {
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 0);
EXPECT_TRUE(slider->OnEvent(MousePressed(3, 0)));
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 0);
EXPECT_EQ(value, 30);
EXPECT_EQ(updated, 1);
EXPECT_TRUE(slider->OnEvent(MousePressed(9, 0)));
EXPECT_EQ(value, 90);
EXPECT_EQ(updated, 1);
EXPECT_EQ(updated, 2);
EXPECT_TRUE(slider->OnEvent(MousePressed(9, 2)));
EXPECT_EQ(value, 90);
EXPECT_EQ(updated, 1);
EXPECT_EQ(updated, 2);
EXPECT_TRUE(slider->OnEvent(MousePressed(5, 2)));
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 2);
EXPECT_EQ(updated, 3);
EXPECT_TRUE(slider->OnEvent(MouseReleased(5, 2)));
EXPECT_FALSE(slider->OnEvent(MousePressed(5, 2)));
EXPECT_EQ(value, 50);
@@ -92,17 +92,17 @@ TEST(SliderTest, Left) {
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 0);
EXPECT_TRUE(slider->OnEvent(MousePressed(3, 0)));
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 0);
EXPECT_EQ(value, 70);
EXPECT_EQ(updated, 1);
EXPECT_TRUE(slider->OnEvent(MousePressed(9, 0)));
EXPECT_EQ(value, 10);
EXPECT_EQ(updated, 1);
EXPECT_EQ(updated, 2);
EXPECT_TRUE(slider->OnEvent(MousePressed(9, 2)));
EXPECT_EQ(value, 10);
EXPECT_EQ(updated, 1);
EXPECT_EQ(updated, 2);
EXPECT_TRUE(slider->OnEvent(MousePressed(5, 2)));
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 2);
EXPECT_EQ(updated, 3);
EXPECT_TRUE(slider->OnEvent(MouseReleased(5, 2)));
EXPECT_FALSE(slider->OnEvent(MousePressed(5, 2)));
EXPECT_EQ(value, 50);
@@ -124,21 +124,21 @@ TEST(SliderTest, Down) {
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 0);
EXPECT_TRUE(slider->OnEvent(MousePressed(0, 3)));
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 0);
EXPECT_EQ(value, 30);
EXPECT_EQ(updated, 1);
EXPECT_TRUE(slider->OnEvent(MousePressed(0, 9)));
EXPECT_EQ(value, 90);
EXPECT_EQ(updated, 1);
EXPECT_EQ(updated, 2);
EXPECT_TRUE(slider->OnEvent(MousePressed(2, 9)));
EXPECT_EQ(value, 90);
EXPECT_EQ(updated, 1);
EXPECT_EQ(updated, 2);
EXPECT_TRUE(slider->OnEvent(MousePressed(2, 5)));
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 2);
EXPECT_EQ(updated, 3);
EXPECT_TRUE(slider->OnEvent(MouseReleased(2, 5)));
EXPECT_FALSE(slider->OnEvent(MousePressed(2, 5)));
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 2);
EXPECT_EQ(updated, 3);
}
TEST(SliderTest, Up) {
@@ -157,17 +157,17 @@ TEST(SliderTest, Up) {
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 0);
EXPECT_TRUE(slider->OnEvent(MousePressed(0, 3)));
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 0);
EXPECT_EQ(value, 70);
EXPECT_EQ(updated, 1);
EXPECT_TRUE(slider->OnEvent(MousePressed(0, 9)));
EXPECT_EQ(value, 10);
EXPECT_EQ(updated, 1);
EXPECT_EQ(updated, 2);
EXPECT_TRUE(slider->OnEvent(MousePressed(2, 9)));
EXPECT_EQ(value, 10);
EXPECT_EQ(updated, 1);
EXPECT_EQ(updated, 2);
EXPECT_TRUE(slider->OnEvent(MousePressed(2, 5)));
EXPECT_EQ(value, 50);
EXPECT_EQ(updated, 2);
EXPECT_EQ(updated, 3);
EXPECT_TRUE(slider->OnEvent(MouseReleased(2, 5)));
EXPECT_FALSE(slider->OnEvent(MousePressed(2, 5)));
EXPECT_EQ(value, 50);

View File

@@ -93,7 +93,7 @@ class ResizeDecorator : public NodeDecorator {
Element DefaultRenderState(const WindowRenderState& state) {
Element element = state.inner;
if (state.active) {
if (!state.active) {
element |= dim;
}
@@ -124,7 +124,7 @@ class WindowImpl : public ComponentBase, public WindowOptions {
}
private:
Element Render() final {
Element OnRender() final {
auto element = ComponentBase::Render();
const bool captureable =

View File

@@ -54,10 +54,10 @@ class Border : public Node {
requirement_.min_x =
std::max(requirement_.min_x, children_[1]->requirement().min_x + 2);
}
requirement_.selected_box.x_min++;
requirement_.selected_box.x_max++;
requirement_.selected_box.y_min++;
requirement_.selected_box.y_max++;
requirement_.focused.box.x_min++;
requirement_.focused.box.x_max++;
requirement_.focused.box.y_min++;
requirement_.focused.box.y_max++;
}
void SetBox(Box box) override {
@@ -65,7 +65,8 @@ class Border : public Node {
if (children_.size() == 2) {
Box title_box;
title_box.x_min = box.x_min + 1;
title_box.x_max = std::min(box.x_max - 1, box.x_min + children_[1]->requirement().min_x);
title_box.x_max = std::min(box.x_max - 1,
box.x_min + children_[1]->requirement().min_x);
title_box.y_min = box.y_min;
title_box.y_max = box.y_min;
children_[1]->SetBox(title_box);
@@ -145,10 +146,8 @@ class BorderPixel : public Node {
requirement_.min_x =
std::max(requirement_.min_x, children_[1]->requirement().min_x + 2);
}
requirement_.selected_box.x_min++;
requirement_.selected_box.x_max++;
requirement_.selected_box.y_min++;
requirement_.selected_box.y_max++;
requirement_.focused.box.Shift(1, 1);
}
void SetBox(Box box) override {

View File

@@ -5,6 +5,7 @@
#define FTXUI_DOM_BOX_HELPER_HPP
#include <vector>
#include "ftxui/dom/requirement.hpp"
namespace ftxui::box_helper {
@@ -19,7 +20,6 @@ struct Element {
};
void Compute(std::vector<Element>* elements, int target_size);
} // namespace ftxui::box_helper
#endif /* end of include guard: FTXUI_DOM_BOX_HELPER_HPP */

View File

@@ -21,24 +21,20 @@ class DBox : public Node {
explicit DBox(Elements children) : Node(std::move(children)) {}
void ComputeRequirement() override {
requirement_.min_x = 0;
requirement_.min_y = 0;
requirement_.flex_grow_x = 0;
requirement_.flex_grow_y = 0;
requirement_.flex_shrink_x = 0;
requirement_.flex_shrink_y = 0;
requirement_.selection = Requirement::NORMAL;
requirement_ = Requirement{};
for (auto& child : children_) {
child->ComputeRequirement();
// Propagate the focused requirement.
if (requirement_.focused.Prefer(child->requirement().focused)) {
requirement_.focused = child->requirement().focused;
}
// Extend the min_x and min_y to contain all the children
requirement_.min_x =
std::max(requirement_.min_x, child->requirement().min_x);
requirement_.min_y =
std::max(requirement_.min_y, child->requirement().min_y);
if (requirement_.selection < child->requirement().selection) {
requirement_.selection = child->requirement().selection;
requirement_.selected_box = child->requirement().selected_box;
}
}
}
@@ -79,6 +75,7 @@ class DBox : public Node {
acc->bold = pixel.bold;
acc->dim = pixel.dim;
acc->inverted = pixel.inverted;
acc->italic = pixel.italic;
acc->underlined = pixel.underlined;
acc->underlined_double = pixel.underlined_double;
acc->strikethrough = pixel.strikethrough;

View File

@@ -80,6 +80,7 @@ class Flex : public Node {
}
void SetBox(Box box) override {
Node::SetBox(box);
if (children_.empty()) {
return;
}

View File

@@ -4,6 +4,7 @@
#include <algorithm> // for min, max
#include <cstddef> // for size_t
#include <memory> // for __shared_ptr_access, shared_ptr, allocator_traits<>::value_type, make_shared
#include <tuple> // for ignore
#include <utility> // for move, swap
#include <vector> // for vector
@@ -12,6 +13,7 @@
#include "ftxui/dom/flexbox_helper.hpp" // for Block, Global, Compute
#include "ftxui/dom/node.hpp" // for Node, Elements, Node::Status
#include "ftxui/dom/requirement.hpp" // for Requirement
#include "ftxui/dom/selection.hpp" // for Selection
#include "ftxui/screen/box.hpp" // for Box
namespace ftxui {
@@ -89,37 +91,32 @@ class Flexbox : public Node {
}
void ComputeRequirement() override {
requirement_ = Requirement{};
for (auto& child : children_) {
child->ComputeRequirement();
}
flexbox_helper::Global global;
global.config = config_normalized_;
global_ = flexbox_helper::Global();
global_.config = config_normalized_;
if (IsColumnOriented()) {
global.size_x = 100000; // NOLINT
global.size_y = asked_;
global_.size_x = 100000; // NOLINT
global_.size_y = asked_;
} else {
global.size_x = asked_;
global.size_y = 100000; // NOLINT
global_.size_x = asked_;
global_.size_y = 100000; // NOLINT
}
Layout(global, true);
Layout(global_, true);
// Reset:
requirement_.selection = Requirement::Selection::NORMAL;
requirement_.selected_box = Box();
requirement_.min_x = 0;
requirement_.min_y = 0;
if (global.blocks.empty()) {
if (global_.blocks.empty()) {
return;
}
// Compute the union of all the blocks:
Box box;
box.x_min = global.blocks[0].x;
box.y_min = global.blocks[0].y;
box.x_max = global.blocks[0].x + global.blocks[0].dim_x;
box.y_max = global.blocks[0].y + global.blocks[0].dim_y;
for (auto& b : global.blocks) {
box.x_min = global_.blocks[0].x;
box.y_min = global_.blocks[0].y;
box.x_max = global_.blocks[0].x + global_.blocks[0].dim_x;
box.y_max = global_.blocks[0].y + global_.blocks[0].dim_y;
for (auto& b : global_.blocks) {
box.x_min = std::min(box.x_min, b.x);
box.y_min = std::min(box.y_min, b.y);
box.x_max = std::max(box.x_max, b.x + b.dim_x);
@@ -130,19 +127,14 @@ class Flexbox : public Node {
// Find the selection:
for (size_t i = 0; i < children_.size(); ++i) {
if (requirement_.selection >= children_[i]->requirement().selection) {
continue;
if (requirement_.focused.Prefer(children_[i]->requirement().focused)) {
requirement_.focused = children_[i]->requirement().focused;
// Shift |focused.box| according to its position inside this component:
auto& b = global_.blocks[i];
requirement_.focused.box.Shift(b.x, b.y);
requirement_.focused.box =
Box::Intersection(requirement_.focused.box, box);
}
requirement_.selection = children_[i]->requirement().selection;
Box selected_box = children_[i]->requirement().selected_box;
// Shift |selected_box| according to its position inside this component:
auto& b = global.blocks[i];
selected_box.x_min += b.x;
selected_box.y_min += b.y;
selected_box.x_max += b.x;
selected_box.y_max += b.y;
requirement_.selected_box = Box::Intersection(selected_box, box);
}
}
@@ -177,6 +169,43 @@ class Flexbox : public Node {
}
}
void Select(Selection& selection) override {
// If this Node box_ doesn't intersect with the selection, then no
// selection.
if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
return;
}
Selection selection_lines = IsColumnOriented()
? selection.SaturateVertical(box_)
: selection.SaturateHorizontal(box_);
size_t i = 0;
for (auto& line : global_.lines) {
Box box;
box.x_min = box_.x_min + line.x;
box.x_max = box_.x_min + line.x + line.dim_x - 1;
box.y_min = box_.y_min + line.y;
box.y_max = box_.y_min + line.y + line.dim_y - 1;
// If the line box doesn't intersect with the selection, then no
// selection.
if (Box::Intersection(selection.GetBox(), box).IsEmpty()) {
continue;
}
Selection selection_line = IsColumnOriented()
? selection_lines.SaturateHorizontal(box)
: selection_lines.SaturateVertical(box);
for (auto& block : line.blocks) {
std::ignore = block;
children_[i]->Select(selection_line);
i++;
}
}
}
void Check(Status* status) override {
for (auto& child : children_) {
child->Check(status);
@@ -194,6 +223,7 @@ class Flexbox : public Node {
bool need_iteration_ = true;
const FlexboxConfig config_;
const FlexboxConfig config_normalized_;
flexbox_helper::Global global_;
};
} // namespace

View File

@@ -68,6 +68,10 @@ void SymmetryXY(Global& g) {
std::swap(b.x, b.y);
std::swap(b.dim_x, b.dim_y);
}
for (auto& l : g.lines) {
std::swap(l.x, l.y);
std::swap(l.dim_x, l.dim_y);
}
}
void SymmetryX(Global& g) {
@@ -75,6 +79,9 @@ void SymmetryX(Global& g) {
for (auto& b : g.blocks) {
b.x = g.size_x - b.x - b.dim_x;
}
for (auto& l : g.lines) {
l.x = g.size_x - l.x - l.dim_x;
}
}
void SymmetryY(Global& g) {
@@ -82,14 +89,13 @@ void SymmetryY(Global& g) {
for (auto& b : g.blocks) {
b.y = g.size_y - b.y - b.dim_y;
}
for (auto& l : g.lines) {
l.y = g.size_y - l.y - l.dim_y;
}
}
struct Line {
std::vector<Block*> blocks;
};
void SetX(Global& global, std::vector<Line> lines) {
for (auto& line : lines) {
void SetX(Global& global) {
for (auto& line : global.lines) {
std::vector<box_helper::Element> elements;
elements.reserve(line.blocks.size());
for (auto* block : line.blocks) {
@@ -110,19 +116,24 @@ void SetX(Global& global, std::vector<Line> lines) {
int x = 0;
for (size_t i = 0; i < line.blocks.size(); ++i) {
line.blocks[i]->dim_x = elements[i].size;
line.blocks[i]->x = x;
line.blocks[i]->dim_x = elements[i].size;
x += elements[i].size;
x += global.config.gap_x;
}
}
for (auto& line : global.lines) {
line.x = 0;
line.dim_x = global.size_x;
}
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void SetY(Global& g, std::vector<Line> lines) {
void SetY(Global& g) {
std::vector<box_helper::Element> elements;
elements.reserve(lines.size());
for (auto& line : lines) {
elements.reserve(g.lines.size());
for (auto& line : g.lines) {
box_helper::Element element;
element.flex_shrink = line.blocks.front()->flex_shrink_y;
element.flex_grow = line.blocks.front()->flex_grow_y;
@@ -202,9 +213,9 @@ void SetY(Global& g, std::vector<Line> lines) {
}
// [Align items]
for (size_t i = 0; i < lines.size(); ++i) {
for (size_t i = 0; i < g.lines.size(); ++i) {
auto& element = elements[i];
for (auto* block : lines[i].blocks) {
for (auto* block : g.lines[i].blocks) {
const bool stretch =
block->flex_grow_y != 0 ||
g.config.align_content == FlexboxConfig::AlignContent::Stretch;
@@ -237,10 +248,16 @@ void SetY(Global& g, std::vector<Line> lines) {
}
}
}
ys.push_back(g.size_y);
for (size_t i = 0; i < g.lines.size(); ++i) {
g.lines[i].y = ys[i];
g.lines[i].dim_y = ys[i + 1] - ys[i];
}
}
void JustifyContent(Global& g, std::vector<Line> lines) {
for (auto& line : lines) {
void JustifyContent(Global& g) {
for (auto& line : g.lines) {
Block* last = line.blocks.back();
int remaining_space = g.size_x - last->x - last->dim_x;
switch (g.config.justify_content) {
@@ -315,38 +332,36 @@ void Compute2(Global& global) {
void Compute3(Global& global) {
// Step 1: Lay out every elements into rows:
std::vector<Line> lines;
{
Line line;
int x = 0;
line.blocks.reserve(global.blocks.size());
for (auto& block : global.blocks) {
// Does it fit the end of the row?
// No? Then we need to start a new one:
if (x + block.min_size_x > global.size_x) {
x = 0;
if (!line.blocks.empty()) {
lines.push_back(std::move(line));
global.lines.push_back(std::move(line));
}
line = Line();
}
block.line = static_cast<int>(lines.size());
block.line = static_cast<int>(global.lines.size());
block.line_position = static_cast<int>(line.blocks.size());
line.blocks.push_back(&block);
x += block.min_size_x + global.config.gap_x;
}
if (!line.blocks.empty()) {
lines.push_back(std::move(line));
global.lines.push_back(std::move(line));
}
}
// Step 2: Set positions on the X axis.
SetX(global, lines);
JustifyContent(global, lines); // Distribute remaining space.
SetX(global);
JustifyContent(global); // Distribute remaining space.
// Step 3: Set positions on the Y axis.
SetY(global, lines);
SetY(global);
}
} // namespace

View File

@@ -9,6 +9,7 @@
namespace ftxui::flexbox_helper {
// A block is a rectangle in the flexbox.
struct Block {
// Input:
int min_size_x = 0;
@@ -28,8 +29,18 @@ struct Block {
bool overflow = false;
};
// A line is a row of blocks.
struct Line {
std::vector<Block*> blocks;
int x = 0;
int y = 0;
int dim_x = 0;
int dim_y = 0;
};
struct Global {
std::vector<Block> blocks;
std::vector<Line> lines;
FlexboxConfig config;
int size_x;
int size_y;

View File

@@ -36,13 +36,12 @@ Decorator focusPositionRelative(float x, float y) {
void ComputeRequirement() override {
NodeDecorator::ComputeRequirement();
requirement_.selection = Requirement::Selection::NORMAL;
Box& box = requirement_.selected_box;
box.x_min = int(float(requirement_.min_x) * x_);
box.y_min = int(float(requirement_.min_y) * y_);
box.x_max = int(float(requirement_.min_x) * x_);
box.y_max = int(float(requirement_.min_y) * y_);
requirement_.focused.enabled = true;
requirement_.focused.node = this;
requirement_.focused.box.x_min = int(float(requirement_.min_x) * x_);
requirement_.focused.box.y_min = int(float(requirement_.min_y) * y_);
requirement_.focused.box.x_max = int(float(requirement_.min_x) * x_);
requirement_.focused.box.y_max = int(float(requirement_.min_y) * y_);
}
private:
@@ -75,9 +74,9 @@ Decorator focusPosition(int x, int y) {
void ComputeRequirement() override {
NodeDecorator::ComputeRequirement();
requirement_.selection = Requirement::Selection::NORMAL;
requirement_.focused.enabled = false;
Box& box = requirement_.selected_box;
Box& box = requirement_.focused.box;
box.x_min = x_;
box.y_min = y_;
box.x_max = x_;

View File

@@ -6,28 +6,28 @@
#include <utility> // for move
#include "ftxui/dom/elements.hpp" // for Element, unpack, Elements, focus, frame, select, xframe, yframe
#include "ftxui/dom/node.hpp" // for Node, Elements
#include "ftxui/dom/requirement.hpp" // for Requirement, Requirement::FOCUSED, Requirement::SELECTED
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/screen.hpp" // for Screen, Screen::Cursor
#include "ftxui/util/autoreset.hpp" // for AutoReset
#include "ftxui/dom/node.hpp" // for Node, Elements
#include "ftxui/dom/requirement.hpp" // for Requirement
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/screen.hpp" // for Screen, Screen::Cursor
#include "ftxui/util/autoreset.hpp" // for AutoReset
namespace ftxui {
namespace {
class Select : public Node {
class Focus : public Node {
public:
explicit Select(Elements children) : Node(std::move(children)) {}
explicit Focus(Elements children) : Node(std::move(children)) {}
void ComputeRequirement() override {
Node::ComputeRequirement();
requirement_ = children_[0]->requirement();
auto& selected_box = requirement_.selected_box;
selected_box.x_min = 0;
selected_box.y_min = 0;
selected_box.x_max = requirement_.min_x - 1;
selected_box.y_max = requirement_.min_y - 1;
requirement_.selection = Requirement::SELECTED;
requirement_.focused.enabled = true;
requirement_.focused.node = this;
requirement_.focused.box.x_min = 0;
requirement_.focused.box.y_min = 0;
requirement_.focused.box.x_max = requirement_.min_x - 1;
requirement_.focused.box.y_max = requirement_.min_y - 1;
}
void SetBox(Box box) override {
@@ -36,65 +36,21 @@ class Select : public Node {
}
};
class Focus : public Select {
public:
using Select::Select;
void ComputeRequirement() override {
Select::ComputeRequirement();
requirement_.selection = Requirement::FOCUSED;
}
void Render(Screen& screen) override {
Select::Render(screen);
// Setting the cursor to the right position allow folks using CJK (China,
// Japanese, Korean, ...) characters to see their [input method editor]
// displayed at the right location. See [issue].
//
// [input method editor]:
// https://en.wikipedia.org/wiki/Input_method
//
// [issue]:
// https://github.com/ArthurSonzogni/FTXUI/issues/2#issuecomment-505282355
//
// Unfortunately, Microsoft terminal do not handle properly hidding the
// cursor. Instead the character under the cursor is hidden, which is a big
// problem. As a result, we can't enable setting cursor to the right
// location. It will be displayed at the bottom right corner.
// See:
// https://github.com/microsoft/terminal/issues/1203
// https://github.com/microsoft/terminal/issues/3093
#if !defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
screen.SetCursor(Screen::Cursor{
box_.x_min,
box_.y_min,
Screen::Cursor::Shape::Hidden,
});
#endif
}
};
class Frame : public Node {
public:
Frame(Elements children, bool x_frame, bool y_frame)
: Node(std::move(children)), x_frame_(x_frame), y_frame_(y_frame) {}
void ComputeRequirement() override {
Node::ComputeRequirement();
requirement_ = children_[0]->requirement();
}
void SetBox(Box box) override {
Node::SetBox(box);
auto& selected_box = requirement_.selected_box;
auto& focused_box = requirement_.focused.box;
Box children_box = box;
if (x_frame_) {
const int external_dimx = box.x_max - box.x_min;
const int internal_dimx = std::max(requirement_.min_x, external_dimx);
const int focused_dimx = selected_box.x_max - selected_box.x_min;
int dx = selected_box.x_min - external_dimx / 2 + focused_dimx / 2;
const int focused_dimx = focused_box.x_max - focused_box.x_min;
int dx = focused_box.x_min - external_dimx / 2 + focused_dimx / 2;
dx = std::max(0, std::min(internal_dimx - external_dimx - 1, dx));
children_box.x_min = box.x_min - dx;
children_box.x_max = box.x_min + internal_dimx - dx;
@@ -103,8 +59,8 @@ class Frame : public Node {
if (y_frame_) {
const int external_dimy = box.y_max - box.y_min;
const int internal_dimy = std::max(requirement_.min_y, external_dimy);
const int focused_dimy = selected_box.y_max - selected_box.y_min;
int dy = selected_box.y_min - external_dimy / 2 + focused_dimy / 2;
const int focused_dimy = focused_box.y_max - focused_box.y_min;
int dy = focused_box.y_min - external_dimy / 2 + focused_dimy / 2;
dy = std::max(0, std::min(internal_dimy - external_dimy - 1, dy));
children_box.y_min = box.y_min - dy;
children_box.y_max = box.y_min + internal_dimy - dy;
@@ -130,33 +86,29 @@ class FocusCursor : public Focus {
: Focus(std::move(children)), shape_(shape) {}
private:
void Render(Screen& screen) override {
Select::Render(screen); // NOLINT
screen.SetCursor(Screen::Cursor{
box_.x_min,
box_.y_min,
shape_,
});
void ComputeRequirement() override {
Focus::ComputeRequirement(); // NOLINT
requirement_.focused.cursor_shape = shape_;
}
Screen::Cursor::Shape shape_;
};
} // namespace
/// @brief Set the `child` to be the one selected among its siblings.
/// @param child The element to be selected.
/// @ingroup dom
Element select(Element child) {
return std::make_shared<Select>(unpack(std::move(child)));
}
/// @brief Set the `child` to be the one in focus globally.
/// @brief Set the `child` to be the one focused among its siblings.
/// @param child The element to be focused.
/// @ingroup dom
Element focus(Element child) {
return std::make_shared<Focus>(unpack(std::move(child)));
}
/// This is deprecated. Use `focus` instead.
/// @brief Set the `child` to be the one focused among its siblings.
/// @param child The element to be focused.
Element select(Element child) {
return focus(std::move(child));
}
/// @brief Allow an element to be displayed inside a 'virtual' area. It size can
/// be larger than its container. In this case only a smaller portion is
/// displayed. The view is scrollable to make the focused element visible.

View File

@@ -49,13 +49,7 @@ class GridBox : public Node {
}
void ComputeRequirement() override {
requirement_.min_x = 0;
requirement_.min_y = 0;
requirement_.flex_grow_x = 0;
requirement_.flex_grow_y = 0;
requirement_.flex_shrink_x = 0;
requirement_.flex_shrink_y = 0;
requirement_ = Requirement{};
for (auto& line : lines_) {
for (auto& cell : line) {
cell->ComputeRequirement();
@@ -75,19 +69,15 @@ class GridBox : public Node {
requirement_.min_x = Integrate(size_x);
requirement_.min_y = Integrate(size_y);
// Forward the selected/focused child state:
requirement_.selection = Requirement::NORMAL;
// Forward the focused/focused child state:
for (int x = 0; x < x_size; ++x) {
for (int y = 0; y < y_size; ++y) {
if (requirement_.selection >= lines_[y][x]->requirement().selection) {
if (requirement_.focused.enabled ||
!lines_[y][x]->requirement().focused.enabled) {
continue;
}
requirement_.selection = lines_[y][x]->requirement().selection;
requirement_.selected_box = lines_[y][x]->requirement().selected_box;
requirement_.selected_box.x_min += size_x[x];
requirement_.selected_box.x_max += size_x[x];
requirement_.selected_box.y_min += size_y[y];
requirement_.selected_box.y_max += size_y[y];
requirement_.focused = lines_[y][x]->requirement().focused;
requirement_.focused.box.Shift(size_x[x], size_y[y]);
}
}
}

View File

@@ -7,7 +7,7 @@
#include <string> // for allocator, basic_string, string
#include <vector> // for vector
#include "ftxui/dom/elements.hpp" // for text, operator|, Element, flex, Elements, flex_grow, flex_shrink, vtext, gridbox, vbox, focus, operator|=, border, frame
#include "ftxui/dom/elements.hpp" // for text, operator|, Element, flex, Elements, flex_grow, flex_shrink, vtext, gridbox, vbox, select, operator|=, border, frame
#include "ftxui/dom/node.hpp" // for Render
#include "ftxui/screen/screen.hpp" // for Screen

View File

@@ -11,8 +11,8 @@
#include "ftxui/dom/elements.hpp" // for Element, Elements, hbox
#include "ftxui/dom/node.hpp" // for Node, Elements
#include "ftxui/dom/requirement.hpp" // for Requirement
#include "ftxui/dom/selection.hpp" // for Selection
#include "ftxui/screen/box.hpp" // for Box
namespace ftxui {
namespace {
@@ -20,22 +20,20 @@ class HBox : public Node {
public:
explicit HBox(Elements children) : Node(std::move(children)) {}
private:
void ComputeRequirement() override {
requirement_.min_x = 0;
requirement_.min_y = 0;
requirement_.flex_grow_x = 0;
requirement_.flex_grow_y = 0;
requirement_.flex_shrink_x = 0;
requirement_.flex_shrink_y = 0;
requirement_.selection = Requirement::NORMAL;
requirement_ = Requirement{};
for (auto& child : children_) {
child->ComputeRequirement();
if (requirement_.selection < child->requirement().selection) {
requirement_.selection = child->requirement().selection;
requirement_.selected_box = child->requirement().selected_box;
requirement_.selected_box.x_min += requirement_.min_x;
requirement_.selected_box.x_max += requirement_.min_x;
// Propagate the focused requirement.
if (requirement_.focused.Prefer(child->requirement().focused)) {
requirement_.focused = child->requirement().focused;
requirement_.focused.box.Shift(requirement_.min_x, 0);
}
// Extend the min_x and min_y to contain all the children
requirement_.min_x += child->requirement().min_x;
requirement_.min_y =
std::max(requirement_.min_y, child->requirement().min_y);
@@ -64,6 +62,19 @@ class HBox : public Node {
x = box.x_max + 1;
}
}
void Select(Selection& selection) override {
// If this Node box_ doesn't intersect with the selection, then no
// selection.
if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
return;
}
Selection selection_saturated = selection.SaturateHorizontal(box_);
for (auto& child : children_) {
child->Select(selection_saturated);
}
}
};
} // namespace

35
src/ftxui/dom/italic.cpp Normal file
View File

@@ -0,0 +1,35 @@
// Copyright 2025 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <memory> // for make_shared
#include <utility> // for move
#include "ftxui/dom/elements.hpp" // for Element, underlinedDouble
#include "ftxui/dom/node.hpp" // for Node
#include "ftxui/dom/node_decorator.hpp" // for NodeDecorator
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/screen.hpp" // for Pixel, Screen
namespace ftxui {
/// @brief Apply a underlinedDouble to text.
/// @ingroup dom
Element italic(Element child) {
class Impl : public NodeDecorator {
public:
using NodeDecorator::NodeDecorator;
void Render(Screen& screen) override {
for (int y = box_.y_min; y <= box_.y_max; ++y) {
for (int x = box_.x_min; x <= box_.x_max; ++x) {
screen.PixelAt(x, y).italic = true;
}
}
Node::Render(screen);
}
};
return std::make_shared<Impl>(std::move(child));
}
} // namespace ftxui

View File

@@ -0,0 +1,22 @@
// Copyright 2025 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <string> // for allocator, string
#include "ftxui/dom/elements.hpp" // for operator|, text, bold, Element
#include "ftxui/dom/node.hpp" // for Render
#include "ftxui/screen/screen.hpp" // for Screen, Pixel
#include "gtest/gtest.h" // for Test, AssertionResult, EXPECT_TRUE, Message, TEST, TestPartResult
// NOLINTBEGIN
namespace ftxui {
TEST(ItalicTest, Basic) {
auto element = text("text") | italic;
Screen screen(5, 1);
Render(screen, element);
EXPECT_TRUE(screen.PixelAt(0, 0).italic);
}
} // namespace ftxui
// NOLINTEND

View File

@@ -97,7 +97,11 @@ Color Interpolate(const LinearGradientNormalized& gradient, float t) {
// Find the right color in the gradient's stops.
size_t i = 1;
while (true) {
if (i > gradient.positions.size()) {
// Note that `t` might be slightly greater than 1.0 due to floating point
// precision. This is why we need to handle the case where `t` is greater
// than the last stop's position.
// See https://github.com/ArthurSonzogni/FTXUI/issues/998
if (i >= gradient.positions.size()) {
const float half = 0.5F;
return Color::Interpolate(half, gradient.colors.back(),
gradient.colors.back());

View File

@@ -2,9 +2,12 @@
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <ftxui/screen/box.hpp> // for Box
#include <utility> // for move
#include <string>
#include <utility> // for move
#include <cstddef>
#include "ftxui/dom/node.hpp"
#include "ftxui/dom/selection.hpp" // for Selection
#include "ftxui/screen/screen.hpp" // for Screen
namespace ftxui {
@@ -16,9 +19,23 @@ Node::~Node() = default;
/// @brief Compute how much space an elements needs.
/// @ingroup dom
void Node::ComputeRequirement() {
if (children_.empty()) {
return;
}
for (auto& child : children_) {
child->ComputeRequirement();
}
// By default, the requirement is the one of the first child.
requirement_ = children_[0]->requirement();
// Propagate the focused requirement.
for (size_t i = 1; i < children_.size(); ++i) {
if (!requirement_.focused.enabled &&
children_[i]->requirement().focused.enabled) {
requirement_.focused = children_[i]->requirement().focused;
}
}
}
/// @brief Assign a position and a dimension to an element for drawing.
@@ -27,6 +44,20 @@ void Node::SetBox(Box box) {
box_ = box;
}
/// @brief Compute the selection of an element.
/// @ingroup dom
void Node::Select(Selection& selection) {
// If this Node box_ doesn't intersect with the selection, then no selection.
if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
return;
}
// By default we defer the selection to the children.
for (auto& child : children_) {
child->Select(selection);
}
}
/// @brief Display an element on a ftxui::Screen.
/// @ingroup dom
void Node::Render(Screen& screen) {
@@ -42,15 +73,31 @@ void Node::Check(Status* status) {
status->need_iteration |= (status->iteration == 0);
}
std::string Node::GetSelectedContent(Selection& selection) {
std::string content;
for (auto& child : children_) {
content += child->GetSelectedContent(selection);
}
return content;
}
/// @brief Display an element on a ftxui::Screen.
/// @ingroup dom
void Render(Screen& screen, const Element& element) {
Render(screen, element.get());
Selection selection;
Render(screen, element.get(), selection);
}
/// @brief Display an element on a ftxui::Screen.
/// @ingroup dom
void Render(Screen& screen, Node* node) {
Selection selection;
Render(screen, node, selection);
}
void Render(Screen& screen, Node* node, Selection& selection) {
Box box;
box.x_min = 0;
box.y_min = 0;
@@ -73,12 +120,85 @@ void Render(Screen& screen, Node* node) {
node->Check(&status);
}
// Step 3: Draw the element.
// Step 3: Selection
if (!selection.IsEmpty()) {
node->Select(selection);
}
if (node->requirement().focused.enabled
#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
// Setting the cursor to the right position allow folks using CJK (China,
// Japanese, Korean, ...) characters to see their [input method editor]
// displayed at the right location. See [issue].
//
// [input method editor]:
// https://en.wikipedia.org/wiki/Input_method
//
// [issue]:
// https://github.com/ArthurSonzogni/FTXUI/issues/2#issuecomment-505282355
//
// Unfortunately, Microsoft terminal do not handle properly hiding the
// cursor. Instead the character under the cursor is hidden, which is a
// big problem. As a result, we can't enable setting cursor to the right
// location. It will be displayed at the bottom right corner.
// See:
// https://github.com/microsoft/terminal/issues/1203
// https://github.com/microsoft/terminal/issues/3093
&&
node->requirement().focused.cursor_shape != Screen::Cursor::Shape::Hidden
#endif
) {
screen.SetCursor(Screen::Cursor{
node->requirement().focused.node->box_.x_max,
node->requirement().focused.node->box_.y_max,
node->requirement().focused.cursor_shape,
});
} else {
screen.SetCursor(Screen::Cursor{
screen.dimx() - 1,
screen.dimy() - 1,
Screen::Cursor::Shape::Hidden,
});
}
// Step 4: Draw the element.
screen.stencil = box;
node->Render(screen);
// Step 4: Apply shaders
// Step 5: Apply shaders
screen.ApplyShader();
}
std::string GetNodeSelectedContent(Screen& screen,
Node* node,
Selection& selection) {
Box box;
box.x_min = 0;
box.y_min = 0;
box.x_max = screen.dimx() - 1;
box.y_max = screen.dimy() - 1;
Node::Status status;
node->Check(&status);
const int max_iterations = 20;
while (status.need_iteration && status.iteration < max_iterations) {
// Step 1: Find what dimension this elements wants to be.
node->ComputeRequirement();
// Step 2: Assign a dimension to the element.
node->SetBox(box);
// Check if the element needs another iteration of the layout algorithm.
status.need_iteration = false;
status.iteration++;
node->Check(&status);
}
// Step 3: Selection
node->Select(selection);
// Step 4: get the selected content.
return node->GetSelectedContent(selection);
}
} // namespace ftxui

View File

@@ -1,9 +1,10 @@
// Copyright 2020 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <sstream> // for basic_istream, stringstream
#include <string> // for string, allocator, getline
#include <utility> // for move
#include <functional> // for function
#include <sstream> // for basic_istream, stringstream
#include <string> // for string, allocator, getline
#include <utility> // for move
#include "ftxui/dom/elements.hpp" // for flexbox, Element, text, Elements, operator|, xflex, paragraph, paragraphAlignCenter, paragraphAlignJustify, paragraphAlignLeft, paragraphAlignRight
#include "ftxui/dom/flexbox_config.hpp" // for FlexboxConfig, FlexboxConfig::JustifyContent, FlexboxConfig::JustifyContent::Center, FlexboxConfig::JustifyContent::FlexEnd, FlexboxConfig::JustifyContent::SpaceBetween
@@ -20,6 +21,18 @@ Elements Split(const std::string& the_text) {
}
return output;
}
Element Split(const std::string& paragraph,
const std::function<Element(std::string)>& f) {
Elements output;
std::stringstream ss(paragraph);
std::string line;
while (std::getline(ss, line, '\n')) {
output.push_back(f(line));
}
return vbox(std::move(output));
}
} // namespace
/// @brief Return an element drawing the paragraph on multiple lines.
@@ -34,18 +47,22 @@ Element paragraph(const std::string& the_text) {
/// @ingroup dom
/// @see flexbox.
Element paragraphAlignLeft(const std::string& the_text) {
static const auto config = FlexboxConfig().SetGap(1, 0);
return flexbox(Split(the_text), config);
}
return Split(the_text, [](const std::string& line) {
static const auto config = FlexboxConfig().SetGap(1, 0);
return flexbox(Split(line), config);
});
};
/// @brief Return an element drawing the paragraph on multiple lines, aligned on
/// the right.
/// @ingroup dom
/// @see flexbox.
Element paragraphAlignRight(const std::string& the_text) {
static const auto config =
FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::FlexEnd);
return flexbox(Split(the_text), config);
return Split(the_text, [](const std::string& line) {
static const auto config = FlexboxConfig().SetGap(1, 0).Set(
FlexboxConfig::JustifyContent::FlexEnd);
return flexbox(Split(line), config);
});
}
/// @brief Return an element drawing the paragraph on multiple lines, aligned on
@@ -53,9 +70,11 @@ Element paragraphAlignRight(const std::string& the_text) {
/// @ingroup dom
/// @see flexbox.
Element paragraphAlignCenter(const std::string& the_text) {
static const auto config =
FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::Center);
return flexbox(Split(the_text), config);
return Split(the_text, [](const std::string& line) {
static const auto config =
FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::Center);
return flexbox(Split(line), config);
});
}
/// @brief Return an element drawing the paragraph on multiple lines, aligned
@@ -64,11 +83,13 @@ Element paragraphAlignCenter(const std::string& the_text) {
/// @ingroup dom
/// @see flexbox.
Element paragraphAlignJustify(const std::string& the_text) {
static const auto config = FlexboxConfig().SetGap(1, 0).Set(
FlexboxConfig::JustifyContent::SpaceBetween);
Elements words = Split(the_text);
words.push_back(text("") | xflex);
return flexbox(std::move(words), config);
return Split(the_text, [](const std::string& line) {
static const auto config = FlexboxConfig().SetGap(1, 0).Set(
FlexboxConfig::JustifyContent::SpaceBetween);
Elements words = Split(line);
words.push_back(text("") | xflex);
return flexbox(std::move(words), config);
});
}
} // namespace ftxui

View File

@@ -5,7 +5,7 @@
#include <string> // for allocator, to_string, string
#include <utility> // for move
#include "ftxui/dom/elements.hpp" // for operator|, Element, operator|=, text, vbox, Elements, border, focus, frame, vscroll_indicator
#include "ftxui/dom/elements.hpp" // for operator|, Element, operator|=, text, vbox, Elements, border, select, frame, vscroll_indicator
#include "ftxui/dom/node.hpp" // for Render
#include "ftxui/screen/color.hpp" // for Color, Color::Red
#include "ftxui/screen/screen.hpp" // for Screen

173
src/ftxui/dom/selection.cpp Normal file
View File

@@ -0,0 +1,173 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include "ftxui/dom/selection.hpp" // for Selection
#include <algorithm> // for max, min
#include <string> // for string
#include <tuple> // for ignore
#include "ftxui/dom/node_decorator.hpp" // for NodeDecorator
namespace ftxui {
namespace {
class Unselectable : public NodeDecorator {
public:
using NodeDecorator::NodeDecorator;
void Select(Selection& ignored) override {
std::ignore = ignored;
// Overwrite the select method to do nothing.
}
};
} // namespace
/// @brief Create an empty selection.
Selection::Selection() = default;
/// @brief Create a selection.
/// @param start_x The x coordinate of the start of the selection.
/// @param start_y The y coordinate of the start of the selection.
/// @param end_x The x coordinate of the end of the selection.
/// @param end_y The y coordinate of the end of the selection.
Selection::Selection(int start_x, int start_y, int end_x, int end_y)
: start_x_(start_x),
start_y_(start_y),
end_x_(end_x),
end_y_(end_y),
box_{
std::min(start_x, end_x),
std::max(start_x, end_x),
std::min(start_y, end_y),
std::max(start_y, end_y),
},
empty_(false) {}
Selection::Selection(int start_x,
int start_y,
int end_x,
int end_y,
Selection* parent)
: start_x_(start_x),
start_y_(start_y),
end_x_(end_x),
end_y_(end_y),
box_{
std::min(start_x, end_x),
std::max(start_x, end_x),
std::min(start_y, end_y),
std::max(start_y, end_y),
},
parent_(parent),
empty_(false) {}
/// @brief Get the box of the selection.
/// @return The box of the selection.
const Box& Selection::GetBox() const {
return box_;
}
/// @brief Saturate the selection to be inside the box.
/// This is called by `hbox` to propagate the selection to its children.
/// @param box The box to saturate the selection in.
/// @return The saturated selection.
Selection Selection::SaturateHorizontal(Box box) {
int start_x = start_x_;
int start_y = start_y_;
int end_x = end_x_;
int end_y = end_y_;
const bool start_outside = !box.Contain(start_x, start_y);
const bool end_outside = !box.Contain(end_x, end_y);
const bool properly_ordered =
start_y < end_y || (start_y == end_y && start_x <= end_x);
if (properly_ordered) {
if (start_outside) {
start_x = box.x_min;
start_y = box.y_min;
}
if (end_outside) {
end_x = box.x_max;
end_y = box.y_max;
}
} else {
if (start_outside) {
start_x = box.x_max;
start_y = box.y_max;
}
if (end_outside) {
end_x = box.x_min;
end_y = box.y_min;
}
}
return {
start_x, start_y, end_x, end_y, parent_,
};
}
/// @brief Saturate the selection to be inside the box.
/// This is called by `vbox` to propagate the selection to its children.
/// @param box The box to saturate the selection in.
/// @return The saturated selection.
Selection Selection::SaturateVertical(Box box) {
int start_x = start_x_;
int start_y = start_y_;
int end_x = end_x_;
int end_y = end_y_;
const bool start_outside = !box.Contain(start_x, start_y);
const bool end_outside = !box.Contain(end_x, end_y);
const bool properly_ordered =
start_y < end_y || (start_y == end_y && start_x <= end_x);
if (properly_ordered) {
if (start_outside) {
start_x = box.x_min;
start_y = box.y_min;
}
if (end_outside) {
end_x = box.x_max;
end_y = box.y_max;
}
} else {
if (start_outside) {
start_x = box.x_max;
start_y = box.y_max;
}
if (end_outside) {
end_x = box.x_min;
end_y = box.y_min;
}
}
return {start_x, start_y, end_x, end_y, parent_};
}
void Selection::AddPart(const std::string& part, int y, int left, int right) {
if (parent_ != this) {
parent_->AddPart(part, y, left, right);
return;
}
[&] {
if (parts_.str().empty()) {
parts_ << part;
return;
}
if (y_ != y) {
parts_ << '\n' << part;
return;
}
if (x_ == left + 1) {
parts_ << part;
return;
}
parts_ << part;
}();
y_ = y;
x_ = right;
}
} // namespace ftxui

View File

@@ -0,0 +1,92 @@
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <functional> // for function
#include <memory> // for make_shared
#include <utility> // for move
#include "ftxui/dom/elements.hpp" // for Element, Decorator, bgcolor, color
#include "ftxui/dom/node_decorator.hpp" // for NodeDecorator
#include "ftxui/screen/color.hpp" // for Color
#include "ftxui/screen/pixel.hpp" // for Pixel
#include "ftxui/screen/screen.hpp" // for Screen
namespace ftxui {
namespace {
class SelectionStyleReset : public NodeDecorator {
public:
explicit SelectionStyleReset(Element child)
: NodeDecorator(std::move(child)) {}
void Render(Screen& screen) final {
auto old_style = screen.GetSelectionStyle();
screen.SetSelectionStyle([](Pixel&) {});
NodeDecorator::Render(screen);
screen.SetSelectionStyle(old_style);
}
};
class SelectionStyle : public NodeDecorator {
public:
SelectionStyle(Element child, const std::function<void(Pixel&)>& style)
: NodeDecorator(std::move(child)), style_(style) {}
void Render(Screen& screen) final {
auto old_style = screen.GetSelectionStyle();
auto new_style = [&, old_style](Pixel& pixel) {
old_style(pixel);
style_(pixel);
};
screen.SetSelectionStyle(new_style);
NodeDecorator::Render(screen);
screen.SetSelectionStyle(old_style);
}
std::function<void(Pixel&)> style_;
};
} // namespace
/// @brief Reset the selection style of an element.
/// @param child The input element.
/// @return The output element with the selection style reset.
Element selectionStyleReset(Element child) {
return std::make_shared<SelectionStyleReset>(std::move(child));
}
/// @brief Set the background color of an element when selected.
/// Note that the style is applied on top of the existing style.
Decorator selectionBackgroundColor(Color foreground) {
return selectionStyle([foreground](Pixel& pixel) { //
pixel.background_color = foreground;
});
}
/// @brief Set the foreground color of an element when selected.
/// Note that the style is applied on top of the existing style.
Decorator selectionForegroundColor(Color foreground) {
return selectionStyle([foreground](Pixel& pixel) { //
pixel.foreground_color = foreground;
});
}
/// @brief Set the color of an element when selected.
/// @param foreground The color to be applied.
/// Note that the style is applied on top of the existing style.
Decorator selectionColor(Color foreground) {
return selectionForegroundColor(foreground);
}
/// @brief Set the style of an element when selected.
/// @param style The style to be applied.
/// Note that the style is applied on top of the existing style.
// NOLINTNEXTLINE
Decorator selectionStyle(std::function<void(Pixel&)> style) {
return [style](Element child) -> Element {
return std::make_shared<SelectionStyle>(std::move(child), style);
};
}
} // namespace ftxui

View File

@@ -0,0 +1,224 @@
// Copyright 2022 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <gtest/gtest.h>
#include <csignal> // for raise, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM
#include "ftxui/component/component.hpp" // for Input, Renderer, Vertical
#include "ftxui/component/event.hpp" // for Event
#include "ftxui/component/loop.hpp" // for Loop
#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Pressed, Mouse::Released
#include "ftxui/component/screen_interactive.hpp"
#include "ftxui/dom/elements.hpp" // for text
#include "ftxui/dom/node.hpp" // for Render
#include "ftxui/screen/screen.hpp" // for Screen
// NOLINTBEGIN
namespace ftxui {
namespace {
Event MousePressed(int x, int y) {
Mouse mouse;
mouse.button = Mouse::Left;
mouse.motion = Mouse::Pressed;
mouse.shift = false;
mouse.meta = false;
mouse.control = false;
mouse.x = x;
mouse.y = y;
return Event::Mouse("", mouse);
}
Event MouseReleased(int x, int y) {
Mouse mouse;
mouse.button = Mouse::Left;
mouse.motion = Mouse::Released;
mouse.shift = false;
mouse.meta = false;
mouse.control = false;
mouse.x = x;
mouse.y = y;
return Event::Mouse("", mouse);
}
Event MouseMove(int x, int y) {
Mouse mouse;
mouse.button = Mouse::Left;
mouse.motion = Mouse::Moved;
mouse.shift = false;
mouse.meta = false;
mouse.control = false;
mouse.x = x;
mouse.y = y;
return Event::Mouse("", mouse);
}
} // namespace
TEST(SelectionTest, DefaultSelection) {
auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
auto screen = ScreenInteractive::FixedSize(20, 1);
EXPECT_EQ(screen.GetSelection(), "");
Loop loop(&screen, component);
screen.PostEvent(MousePressed(3, 1));
screen.PostEvent(MouseReleased(10, 1));
loop.RunOnce();
EXPECT_EQ(screen.GetSelection(), "rem ipsu");
}
TEST(SelectionTest, SelectionChange) {
int selectionChangeCounter = 0;
auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
auto screen = ScreenInteractive::FixedSize(20, 1);
screen.SelectionChange([&] { selectionChangeCounter++; });
Loop loop(&screen, component);
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 0);
screen.PostEvent(MousePressed(3, 1));
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 0);
screen.PostEvent(MouseMove(5, 1));
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 1);
screen.PostEvent(MouseMove(7, 1));
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 2);
screen.PostEvent(MouseReleased(10, 1));
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 3);
screen.PostEvent(MouseMove(10, 1));
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 3);
EXPECT_EQ(screen.GetSelection(), "rem ipsu");
}
// Check that submitting multiple mouse events quickly doesn't trigger multiple
// selection change events.
TEST(SelectionTest, SelectionOnChangeSquashedEvents) {
int selectionChangeCounter = 0;
auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
auto screen = ScreenInteractive::FixedSize(20, 1);
screen.SelectionChange([&] { selectionChangeCounter++; });
Loop loop(&screen, component);
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 0);
screen.PostEvent(MousePressed(3, 1));
screen.PostEvent(MouseMove(5, 1));
screen.PostEvent(MouseMove(7, 1));
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 1);
screen.PostEvent(MouseReleased(10, 1));
screen.PostEvent(MouseMove(10, 1));
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 2);
EXPECT_EQ(screen.GetSelection(), "rem ipsu");
}
TEST(SelectionTest, StyleSelection) {
int selectionChangeCounter = 0;
auto element = hbox({
text("Lorem "),
text("ipsum") | selectionColor(Color::Red),
text(" dolor"),
});
auto screen = ScreenInteractive::FixedSize(20, 1);
Selection selection(2, 0, 9, 0);
Render(screen, element.get(), selection);
for (int i = 0; i < 20; i++) {
if (i >= 2 && i <= 9) {
EXPECT_EQ(screen.PixelAt(i, 0).inverted, true);
} else {
EXPECT_EQ(screen.PixelAt(i, 0).inverted, false);
}
if (i >= 6 && i <= 9) {
EXPECT_EQ(screen.PixelAt(i, 0).foreground_color, Color::Red);
} else {
EXPECT_EQ(screen.PixelAt(i, 0).foreground_color, Color::Default);
}
}
}
TEST(SelectionTest, VBoxSelection) {
auto element = vbox({
text("Lorem ipsum dolor"),
text("Ut enim ad minim"),
});
auto screen = ScreenInteractive::FixedSize(20, 2);
Selection selection(2, 0, 2, 1);
Render(screen, element.get(), selection);
EXPECT_EQ(selection.GetParts(), "rem ipsum dolor\nUt ");
EXPECT_EQ(screen.ToString(),
"Lo\x1B[7mrem ipsum dolor\x1B[27m \r\n"
"\x1B[7mUt \x1B[27menim ad minim ");
}
TEST(SelectionTest, VBoxSaturatedSelection) {
auto element = vbox({
text("Lorem ipsum dolor"),
text("Ut enim ad minim"),
text("Duis aute irure"),
});
auto screen = ScreenInteractive::FixedSize(20, 3);
Selection selection(2, 0, 2, 2);
Render(screen, element.get(), selection);
EXPECT_EQ(selection.GetParts(), "rem ipsum dolor\nUt enim ad minim\nDui");
EXPECT_EQ(screen.ToString(),
"Lo\x1B[7mrem ipsum dolor\x1B[27m \r\n"
"\x1B[7mUt enim ad minim\x1B[27m \r\n"
"\x1B[7mDui\x1B[27ms aute irure ");
}
TEST(SelectionTest, HBoxSelection) {
auto element = hbox({
text("Lorem ipsum dolor"),
text("Ut enim ad minim"),
});
auto screen = ScreenInteractive::FixedSize(40, 1);
Selection selection(2, 0, 20, 0);
Render(screen, element.get(), selection);
EXPECT_EQ(selection.GetParts(), "rem ipsum dolorUt e");
EXPECT_EQ(screen.ToString(),
"Lo\x1B[7mrem ipsum dolorUt e\x1B[27mnim ad minim ");
}
TEST(SelectionTest, HBoxSaturatedSelection) {
auto element = hbox({
text("Lorem ipsum dolor"),
text("Ut enim ad minim"),
text("Duis aute irure"),
});
auto screen = ScreenInteractive::FixedSize(60, 1);
Selection selection(2, 0, 35, 0);
Render(screen, element.get(), selection);
EXPECT_EQ(selection.GetParts(), "rem ipsum dolorUt enim ad minimDui");
EXPECT_EQ(screen.ToString(),
"Lo\x1B[7mrem ipsum dolorUt enim ad minimDui\x1B[27ms aute irure "
" ");
}
} // namespace ftxui
// NOLINTEND

View File

@@ -3,7 +3,8 @@
// the LICENSE file.
#include "ftxui/dom/table.hpp"
#include <algorithm> // for max
#include <algorithm> // for max
#include <initializer_list> // for initializer_list
#include <memory> // for allocator, shared_ptr, allocator_traits<>::value_type
#include <utility> // for move, swap
#include <vector> // for vector

View File

@@ -3,13 +3,15 @@
// the LICENSE file.
#include <algorithm> // for min
#include <memory> // for make_shared
#include <string> // for string, wstring
#include <utility> // for move
#include <sstream>
#include <string> // for string, wstring
#include <utility> // for move
#include "ftxui/dom/deprecated.hpp" // for text, vtext
#include "ftxui/dom/elements.hpp" // for Element, text, vtext
#include "ftxui/dom/node.hpp" // for Node
#include "ftxui/dom/requirement.hpp" // for Requirement
#include "ftxui/dom/selection.hpp" // for Selection
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/screen.hpp" // for Pixel, Screen
#include "ftxui/screen/string.hpp" // for string_width, Utf8ToGlyphs, to_string
@@ -26,28 +28,67 @@ class Text : public Node {
void ComputeRequirement() override {
requirement_.min_x = string_width(text_);
requirement_.min_y = 1;
has_selection = false;
}
void Select(Selection& selection) override {
if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
return;
}
const Selection selection_saturated = selection.SaturateHorizontal(box_);
has_selection = true;
selection_start_ = selection_saturated.GetBox().x_min;
selection_end_ = selection_saturated.GetBox().x_max;
std::stringstream ss;
int x = box_.x_min;
for (const auto& cell : Utf8ToGlyphs(text_)) {
if (cell == "\n") {
continue;
}
if (selection_start_ <= x && x <= selection_end_) {
ss << cell;
}
x++;
}
selection.AddPart(ss.str(), box_.y_min, selection_start_, selection_end_);
}
void Render(Screen& screen) override {
int x = box_.x_min;
const int y = box_.y_min;
if (y > box_.y_max) {
return;
}
for (const auto& cell : Utf8ToGlyphs(text_)) {
if (x > box_.x_max) {
return;
break;
}
if (cell == "\n") {
continue;
}
screen.PixelAt(x, y).character = cell;
if (has_selection) {
auto selectionTransform = screen.GetSelectionStyle();
if ((x >= selection_start_) && (x <= selection_end_)) {
selectionTransform(screen.PixelAt(x, y));
}
}
++x;
}
}
private:
std::string text_;
bool has_selection = false;
int selection_start_ = 0;
int selection_end_ = -1;
};
class VText : public Node {

View File

@@ -11,6 +11,7 @@
#include "ftxui/dom/elements.hpp" // for Element, Elements, vbox
#include "ftxui/dom/node.hpp" // for Node, Elements
#include "ftxui/dom/requirement.hpp" // for Requirement
#include "ftxui/dom/selection.hpp" // for Selection
#include "ftxui/screen/box.hpp" // for Box
namespace ftxui {
@@ -20,22 +21,20 @@ class VBox : public Node {
public:
explicit VBox(Elements children) : Node(std::move(children)) {}
private:
void ComputeRequirement() override {
requirement_.min_x = 0;
requirement_.min_y = 0;
requirement_.flex_grow_x = 0;
requirement_.flex_grow_y = 0;
requirement_.flex_shrink_x = 0;
requirement_.flex_shrink_y = 0;
requirement_.selection = Requirement::NORMAL;
requirement_ = Requirement{};
for (auto& child : children_) {
child->ComputeRequirement();
if (requirement_.selection < child->requirement().selection) {
requirement_.selection = child->requirement().selection;
requirement_.selected_box = child->requirement().selected_box;
requirement_.selected_box.y_min += requirement_.min_y;
requirement_.selected_box.y_max += requirement_.min_y;
// Propagate the focused requirement.
if (requirement_.focused.Prefer(child->requirement().focused)) {
requirement_.focused = child->requirement().focused;
requirement_.focused.box.Shift(0, requirement_.min_y);
}
// Extend the min_x and min_y to contain all the children
requirement_.min_y += child->requirement().min_y;
requirement_.min_x =
std::max(requirement_.min_x, child->requirement().min_x);
@@ -64,6 +63,20 @@ class VBox : public Node {
y = box.y_max + 1;
}
}
void Select(Selection& selection) override {
// If this Node box_ doesn't intersect with the selection, then no
// selection.
if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
return;
}
Selection selection_saturated = selection.SaturateVertical(box_);
for (auto& child : children_) {
child->Select(selection_saturated);
}
}
};
} // namespace

View File

@@ -30,6 +30,17 @@ Box Box::Union(Box a, Box b) {
};
}
/// Shift the box by (x,y).
/// @param x horizontal shift.
/// @param y vertical shift.
/// @ingroup screen
void Box::Shift(int x, int y) {
x_min += x;
x_max += x;
y_min += y;
y_max += y;
}
/// @return whether (x,y) is contained inside the box.
/// @ingroup screen
bool Box::Contain(int x, int y) const {

View File

@@ -106,6 +106,12 @@ void UpdatePixelStyle(const Screen* screen,
: "\x1B[27m"); // INVERTED_RESET
}
// Italics
if (FTXUI_UNLIKELY(next.italic != prev.italic)) {
ss << (next.italic ? "\x1B[3m" // ITALIC_SET
: "\x1B[23m"); // ITALIC_RESET
}
// StrikeThrough
if (FTXUI_UNLIKELY(next.strikethrough != prev.strikethrough)) {
ss << (next.strikethrough ? "\x1B[9m" // CROSSED_OUT
@@ -544,4 +550,16 @@ const std::string& Screen::Hyperlink(std::uint8_t id) const {
return hyperlinks_[id];
}
/// @brief Return the current selection style.
/// @see SetSelectionStyle
const Screen::SelectionStyle& Screen::GetSelectionStyle() const {
return selection_style_;
}
/// @brief Set the current selection style.
/// @see GetSelectionStyle
void Screen::SetSelectionStyle(SelectionStyle decorator) {
selection_style_ = std::move(decorator);
}
} // namespace ftxui

View File

@@ -1,4 +1,4 @@
// Copyright 2023 Arthur Sonzogni. All rights reserved.
// Copyright 2024 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.