Animation (#355)

This commit is contained in:
Arthur Sonzogni
2022-03-13 18:51:46 +01:00
committed by GitHub
parent 95c766e9e4
commit 4da63b9260
43 changed files with 2439 additions and 654 deletions

View File

@@ -0,0 +1,285 @@
#define _USE_MATH_DEFINES
#include <cmath> // for sin, pow, sqrt, M_PI_2, M_PI, cos
#include "ftxui/component/animation.hpp"
#include <ratio> // for ratio
namespace ftxui {
namespace animation {
namespace easing {
// Easing function have been taken out of:
// https://github.com/warrenm/AHEasing/blob/master/AHEasing/easing.c
//
// Corresponding license:
// Copyright (c) 2011, Auerhaus Development, LLC
//
// This program is free software. It comes without any warranty, to
// the extent permitted by applicable law. You can redistribute it
// and/or modify it under the terms of the Do What The Fuck You Want
// To Public License, Version 2, as published by Sam Hocevar. See
// http://sam.zoy.org/wtfpl/COPYING for more details.
// Modeled after the line y = x
float Linear(float p) {
return p;
}
// Modeled after the parabola y = x^2
float QuadraticIn(float p) {
return p * p;
}
// Modeled after the parabola y = -x^2 + 2x
float QuadraticOut(float p) {
return -(p * (p - 2));
}
// Modeled after the piecewise quadratic
// y = (1/2)((2x)^2) ; [0, 0.5)
// y = -(1/2)((2x-1)*(2x-3) - 1) ; [0.5, 1]
float QuadraticInOut(float p) {
if (p < 0.5) {
return 2 * p * p;
} else {
return (-2 * p * p) + (4 * p) - 1;
}
}
// Modeled after the cubic y = x^3
float CubicIn(float p) {
return p * p * p;
}
// Modeled after the cubic y = (x - 1)^3 + 1
float CubicOut(float p) {
float f = (p - 1);
return f * f * f + 1;
}
// Modeled after the piecewise cubic
// y = (1/2)((2x)^3) ; [0, 0.5)
// y = (1/2)((2x-2)^3 + 2) ; [0.5, 1]
float CubicInOut(float p) {
if (p < 0.5) {
return 4 * p * p * p;
} else {
float f = ((2 * p) - 2);
return 0.5 * f * f * f + 1;
}
}
// Modeled after the quartic x^4
float QuarticIn(float p) {
return p * p * p * p;
}
// Modeled after the quartic y = 1 - (x - 1)^4
float QuarticOut(float p) {
float f = (p - 1);
return f * f * f * (1 - p) + 1;
}
// Modeled after the piecewise quartic
// y = (1/2)((2x)^4) ; [0, 0.5)
// y = -(1/2)((2x-2)^4 - 2) ; [0.5, 1]
float QuarticInOut(float p) {
if (p < 0.5) {
return 8 * p * p * p * p;
} else {
float f = (p - 1);
return -8 * f * f * f * f + 1;
}
}
// Modeled after the quintic y = x^5
float QuinticIn(float p) {
return p * p * p * p * p;
}
// Modeled after the quintic y = (x - 1)^5 + 1
float QuinticOut(float p) {
float f = (p - 1);
return f * f * f * f * f + 1;
}
// Modeled after the piecewise quintic
// y = (1/2)((2x)^5) ; [0, 0.5)
// y = (1/2)((2x-2)^5 + 2) ; [0.5, 1]
float QuinticInOut(float p) {
if (p < 0.5) {
return 16 * p * p * p * p * p;
} else {
float f = ((2 * p) - 2);
return 0.5 * f * f * f * f * f + 1;
}
}
// Modeled after quarter-cycle of sine wave
float SineIn(float p) {
return sin((p - 1) * M_PI_2) + 1;
}
// Modeled after quarter-cycle of sine wave (different phase)
float SineOut(float p) {
return sin(p * M_PI_2);
}
// Modeled after half sine wave
float SineInOut(float p) {
return 0.5 * (1 - cos(p * M_PI));
}
// Modeled after shifted quadrant IV of unit circle
float CircularIn(float p) {
return 1 - sqrt(1 - (p * p));
}
// Modeled after shifted quadrant II of unit circle
float CircularOut(float p) {
return sqrt((2 - p) * p);
}
// Modeled after the piecewise circular function
// y = (1/2)(1 - sqrt(1 - 4x^2)) ; [0, 0.5)
// y = (1/2)(sqrt(-(2x - 3)*(2x - 1)) + 1) ; [0.5, 1]
float CircularInOut(float p) {
if (p < 0.5) {
return 0.5 * (1 - sqrt(1 - 4 * (p * p)));
} else {
return 0.5 * (sqrt(-((2 * p) - 3) * ((2 * p) - 1)) + 1);
}
}
// Modeled after the exponential function y = 2^(10(x - 1))
float ExponentialIn(float p) {
return (p == 0.0) ? p : pow(2, 10 * (p - 1));
}
// Modeled after the exponential function y = -2^(-10x) + 1
float ExponentialOut(float p) {
return (p == 1.0) ? p : 1 - pow(2, -10 * p);
}
// Modeled after the piecewise exponential
// y = (1/2)2^(10(2x - 1)) ; [0,0.5)
// y = -(1/2)*2^(-10(2x - 1))) + 1 ; [0.5,1]
float ExponentialInOut(float p) {
if (p == 0.0 || p == 1.0)
return p;
if (p < 0.5) {
return 0.5 * pow(2, (20 * p) - 10);
} else {
return -0.5 * pow(2, (-20 * p) + 10) + 1;
}
}
// Modeled after the damped sine wave y = sin(13pi/2*x)*pow(2, 10 * (x - 1))
float ElasticIn(float p) {
return sin(13 * M_PI_2 * p) * pow(2, 10 * (p - 1));
}
// Modeled after the damped sine wave y = sin(-13pi/2*(x + 1))*pow(2, -10x) +
// 1
float ElasticOut(float p) {
return sin(-13 * M_PI_2 * (p + 1)) * pow(2, -10 * p) + 1;
}
// Modeled after the piecewise exponentially-damped sine wave:
// y = (1/2)*sin(13pi/2*(2*x))*pow(2, 10 * ((2*x) - 1)) ; [0,0.5)
// y = (1/2)*(sin(-13pi/2*((2x-1)+1))*pow(2,-10(2*x-1)) + 2) ; [0.5, 1]
float ElasticInOut(float p) {
if (p < 0.5) {
return 0.5 * sin(13 * M_PI_2 * (2 * p)) * pow(2, 10 * ((2 * p) - 1));
} else {
return 0.5 *
(sin(-13 * M_PI_2 * ((2 * p - 1) + 1)) * pow(2, -10 * (2 * p - 1)) +
2);
}
}
// Modeled after the overshooting cubic y = x^3-x*sin(x*pi)
float BackIn(float p) {
return p * p * p - p * sin(p * M_PI);
}
// Modeled after overshooting cubic y = 1-((1-x)^3-(1-x)*sin((1-x)*pi))
float BackOut(float p) {
float f = (1 - p);
return 1 - (f * f * f - f * sin(f * M_PI));
}
// Modeled after the piecewise overshooting cubic function:
// y = (1/2)*((2x)^3-(2x)*sin(2*x*pi)) ; [0, 0.5)
// y = (1/2)*(1-((1-x)^3-(1-x)*sin((1-x)*pi))+1) ; [0.5, 1]
float BackInOut(float p) {
if (p < 0.5) {
float f = 2 * p;
return 0.5 * (f * f * f - f * sin(f * M_PI));
} else {
float f = (1 - (2 * p - 1));
return 0.5 * (1 - (f * f * f - f * sin(f * M_PI))) + 0.5;
}
}
float BounceIn(float p) {
return 1 - BounceOut(1 - p);
}
float BounceOut(float p) {
if (p < 4 / 11.0) {
return (121 * p * p) / 16.0;
} else if (p < 8 / 11.0) {
return (363 / 40.0 * p * p) - (99 / 10.0 * p) + 17 / 5.0;
} else if (p < 9 / 10.0) {
return (4356 / 361.0 * p * p) - (35442 / 1805.0 * p) + 16061 / 1805.0;
} else {
return (54 / 5.0 * p * p) - (513 / 25.0 * p) + 268 / 25.0;
}
}
float BounceInOut(float p) {
if (p < 0.5) {
return 0.5 * BounceIn(p * 2);
} else {
return 0.5 * BounceOut(p * 2 - 1) + 0.5;
}
}
} // namespace easing
Animator::Animator(float* from,
float to,
Duration duration,
easing::Function easing_function,
Duration delay)
: value_(from),
from_(*from),
to_(to),
duration_(duration),
easing_function_(easing_function),
current_(-delay) {
RequestAnimationFrame();
}
void Animator::OnAnimation(Params& params) {
current_ += params.duration();
if (current_ >= duration_) {
*value_ = to_;
return;
}
if (current_ <= Duration()) {
*value_ = from_;
} else {
*value_ = from_ + (to_ - from_) * easing_function_(current_ / duration_);
}
RequestAnimationFrame();
}
} // namespace animation
} // namespace ftxui

View File

@@ -1,66 +1,33 @@
#include <functional> // for function
#include <memory> // for shared_ptr
#include <string> // for string
#include <utility> // for move
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
#include "ftxui/component/component.hpp" // for Make, Button
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/component_options.hpp" // for ButtonOption
#include "ftxui/component/event.hpp" // for Event, Event::Return
#include "ftxui/component/animation.hpp" // for Animator, Params (ptr only)
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
#include "ftxui/component/component.hpp" // for Make, Button
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/component_options.hpp" // for ButtonOption, AnimatedColorOption, AnimatedColorsOption
#include "ftxui/component/event.hpp" // for Event, Event::Return
#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Pressed
#include "ftxui/component/screen_interactive.hpp" // for Component
#include "ftxui/dom/elements.hpp" // for operator|, Element, nothing, reflect, text, border, inverted
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/util/ref.hpp" // for ConstStringRef, Ref
#include "ftxui/dom/elements.hpp" // for operator|, Decorator, Element, bgcolor, color, operator|=, reflect, text, border, inverted, nothing
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/color.hpp" // for Color
#include "ftxui/util/ref.hpp" // for Ref, ConstStringRef
namespace ftxui {
namespace {
class ButtonBase : public ComponentBase {
public:
ButtonBase(ConstStringRef label,
std::function<void()> on_click,
Ref<ButtonOption> option)
: label_(label), on_click_(on_click), option_(std::move(option)) {}
// Component implementation:
Element Render() override {
auto style = Focused() ? inverted : nothing;
auto my_border = option_->border ? border : nothing;
return text(*label_) | my_border | style | reflect(box_);
}
bool OnEvent(Event event) override {
if (event.is_mouse() && box_.Contain(event.mouse().x, event.mouse().y)) {
if (!CaptureMouse(event))
return false;
TakeFocus();
if (event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Pressed) {
on_click_();
return true;
}
return false;
}
if (event == Event::Return) {
on_click_();
return true;
}
return false;
}
bool Focusable() const final { return true; }
private:
ConstStringRef label_;
std::function<void()> on_click_;
Box box_;
Ref<ButtonOption> option_;
};
Element DefaultTransform(EntryState params) {
auto element = text(params.label) | border;
if (params.active)
element |= bold;
if (params.focused)
element |= inverted;
return element;
}
} // namespace
@@ -90,7 +57,122 @@ class ButtonBase : public ComponentBase {
Component Button(ConstStringRef label,
std::function<void()> on_click,
Ref<ButtonOption> option) {
return Make<ButtonBase>(label, std::move(on_click), std::move(option));
class Impl : public ComponentBase {
public:
Impl(ConstStringRef label,
std::function<void()> on_click,
Ref<ButtonOption> option)
: label_(label), on_click_(on_click), option_(std::move(option)) {}
// Component implementation:
Element Render() override {
float target = Focused() ? 1.0 : 0.f;
if (target != animator_background_.to())
SetAnimationTarget(target);
EntryState state = {
*label_,
false,
Active(),
Focused(),
};
auto element =
(option_->transform ? option_->transform : DefaultTransform) //
(state);
return element | AnimatedColorStyle() | reflect(box_);
}
Decorator AnimatedColorStyle() {
Decorator style = nothing;
if (option_->animated_colors.background.enabled) {
style = style | bgcolor(Color::Interpolate(
animation_foreground_, //
option_->animated_colors.background.inactive,
option_->animated_colors.background.active));
}
if (option_->animated_colors.foreground.enabled) {
style = style | color(Color::Interpolate(
animation_foreground_, //
option_->animated_colors.foreground.inactive,
option_->animated_colors.foreground.active));
}
return style;
}
void SetAnimationTarget(float target) {
if (option_->animated_colors.foreground.enabled) {
animator_foreground_ =
animation::Animator(&animation_foreground_, target,
option_->animated_colors.foreground.duration,
option_->animated_colors.foreground.function);
}
if (option_->animated_colors.background.enabled) {
animator_background_ =
animation::Animator(&animation_background_, target,
option_->animated_colors.background.duration,
option_->animated_colors.background.function);
}
}
void OnAnimation(animation::Params& p) override {
animator_background_.OnAnimation(p);
animator_foreground_.OnAnimation(p);
}
void OnClick() {
on_click_();
animation_background_ = 0.5f;
animation_foreground_ = 0.5f;
SetAnimationTarget(1.f);
}
bool OnEvent(Event event) override {
if (event.is_mouse())
return OnMouseEvent(event);
if (event == Event::Return) {
OnClick();
return true;
}
return false;
}
bool OnMouseEvent(Event event) {
mouse_hover_ =
box_.Contain(event.mouse().x, event.mouse().y) && CaptureMouse(event);
if (!mouse_hover_)
return false;
TakeFocus();
if (event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Pressed) {
OnClick();
return true;
}
return false;
}
bool Focusable() const final { return true; }
private:
ConstStringRef label_;
std::function<void()> on_click_;
bool mouse_hover_ = false;
Box box_;
Ref<ButtonOption> option_;
float animation_background_ = 0;
float animation_foreground_ = 0;
animation::Animator animator_background_ =
animation::Animator(&animation_background_);
animation::Animator animator_foreground_ =
animation::Animator(&animation_foreground_);
};
return Make<Impl>(label, std::move(on_click), std::move(option));
}
} // namespace ftxui

View File

@@ -18,32 +18,24 @@ namespace {
class CheckboxBase : public ComponentBase {
public:
CheckboxBase(ConstStringRef label, bool* state, Ref<CheckboxOption> option)
: label_(label), state_(state), option_(std::move(option)) {
#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
// Microsoft terminal do not use fonts able to render properly the default
// radiobox glyph.
if (option_->style_checked == "")
option_->style_checked = "[X]";
if (option_->style_unchecked == "")
option_->style_unchecked = "[ ]";
#endif
}
: label_(label), state_(state), option_(std::move(option)) {}
private:
// Component implementation.
Element Render() override {
bool is_focused = Focused();
bool is_active = Active();
auto style = (is_focused || hovered_) ? option_->style_selected_focused
: is_active ? option_->style_selected
: option_->style_normal;
auto focus_management = is_focused ? focus : is_active ? select : nothing;
return hbox({
text(*state_ ? option_->style_checked
: option_->style_unchecked),
text(*label_) | style | focus_management,
}) |
reflect(box_);
auto state = EntryState{
*label_,
*state_,
is_active,
is_focused || hovered_,
};
auto element = (option_->transform
? option_->transform
: CheckboxOption::Simple().transform)(std::move(state));
return element | focus_management | reflect(box_);
}
bool OnEvent(Event event) override {

View File

@@ -31,8 +31,15 @@ Component Collapsible(ConstStringRef label, Component child, Ref<bool> show) {
Impl(ConstStringRef label, Component child, Ref<bool> show)
: label_(label), show_(std::move(show)) {
CheckboxOption opt;
opt.style_checked = "";
opt.style_unchecked = "";
opt.transform = [](EntryState s) {
auto prefix = text(s.state ? "" : "");
auto t = text(s.label);
if (s.active)
t |= bold;
if (s.focused)
t |= inverted;
return hbox({prefix, t});
};
Add(Container::Vertical({
Checkbox(label_, show_.operator->(), opt),
Maybe(std::move(child), show_.operator->()),

View File

@@ -12,6 +12,10 @@
#include "ftxui/component/screen_interactive.hpp" // for Component, ScreenInteractive
#include "ftxui/dom/elements.hpp" // for text, Element
namespace ftxui::animation {
class Params;
} // namespace ftxui::animation
namespace ftxui {
namespace {
@@ -101,6 +105,15 @@ bool ComponentBase::OnEvent(Event event) {
return false;
}
/// @brief Called in response to an animation event.
/// @param animation_params the parameters of the animation
/// The default implementation dispatch the event to every child.
/// @ingroup component
void ComponentBase::OnAnimation(animation::Params& params) {
for (Component& child : children_)
child->OnAnimation(params);
}
/// @brief Return the currently Active child.
/// @return the currently Active child.
/// @ingroup component

View File

@@ -0,0 +1,228 @@
#include "ftxui/component/component_options.hpp"
#include <memory> // for allocator, shared_ptr
#include "ftxui/component/animation.hpp" // for Function, Duration
#include "ftxui/dom/elements.hpp" // for Element, operator|, text, bold, dim, inverted, automerge
namespace ftxui {
void AnimatedColorOption::Set(Color a_inactive,
Color a_active,
animation::Duration a_duration,
animation::easing::Function a_function) {
enabled = true;
inactive = a_inactive;
active = a_active;
duration = a_duration;
function = a_function;
}
void UnderlineOption::SetAnimation(animation::Duration d,
animation::easing::Function f) {
SetAnimationDuration(d);
SetAnimationFunction(f);
}
void UnderlineOption::SetAnimationDuration(animation::Duration d) {
leader_duration = d;
follower_duration = d;
}
void UnderlineOption::SetAnimationFunction(animation::easing::Function f) {
leader_function = f;
follower_function = f;
}
void UnderlineOption::SetAnimationFunction(
animation::easing::Function f_leader,
animation::easing::Function f_follower) {
leader_function = f_leader;
follower_function = f_follower;
}
// static
MenuOption MenuOption::Horizontal() {
MenuOption option;
option.direction = Direction::Right;
option.entries.transform = [](EntryState state) {
Element e = text(state.label);
if (state.focused)
e |= inverted;
if (state.active)
e |= bold;
if (!state.focused && !state.active)
e |= dim;
return e;
};
option.elements_infix = [] { return text(" "); };
return option;
}
// static
MenuOption MenuOption::HorizontalAnimated() {
auto option = Horizontal();
option.underline.enabled = true;
return option;
}
// static
MenuOption MenuOption::Vertical() {
MenuOption option;
option.entries.transform = [](EntryState state) {
if (state.active)
state.label = "> " + state.label;
else
state.label = " " + state.label;
Element e = text(state.label);
if (state.focused)
e |= inverted;
if (state.active)
e |= bold;
if (!state.focused && !state.active)
e |= dim;
return e;
};
return option;
}
// static
MenuOption MenuOption::VerticalAnimated() {
auto option = MenuOption::Vertical();
option.entries.transform = [](EntryState state) {
Element e = text(state.label);
if (state.focused)
e |= inverted;
if (state.active)
e |= bold;
if (!state.focused && !state.active)
e |= dim;
return e;
};
option.underline.enabled = true;
return option;
}
// static
MenuOption MenuOption::Toggle() {
auto option = MenuOption::Horizontal();
option.elements_infix = [] { return text("") | automerge; };
return option;
}
/// @brief Create a ButtonOption, highlighted using [] characters.
// static
ButtonOption ButtonOption::Ascii() {
ButtonOption option;
option.transform = [](EntryState s) {
s.label = s.focused ? "[" + s.label + "]" //
: " " + s.label + " ";
return text(s.label);
};
return option;
}
/// @brief Create a ButtonOption, inverted when focused.
// static
ButtonOption ButtonOption::Simple() {
ButtonOption option;
option.transform = [](EntryState s) {
auto element = text(s.label) | borderLight;
if (s.focused)
element |= inverted;
return element;
};
return option;
}
/// @brief Create a ButtonOption, using animated colors.
// static
ButtonOption ButtonOption::Animated() {
return Animated(Color::Black, Color::GrayLight, //
Color::GrayDark, Color::White);
}
/// @brief Create a ButtonOption, using animated colors.
// static
ButtonOption ButtonOption::Animated(Color color) {
return ButtonOption::Animated(Color::Interpolate(0.85f, color, Color::Black),
Color::Interpolate(0.10f, color, Color::White),
Color::Interpolate(0.10f, color, Color::Black),
Color::Interpolate(0.85f, color, Color::White));
}
/// @brief Create a ButtonOption, using animated colors.
// static
ButtonOption ButtonOption::Animated(Color background, Color foreground) {
return ButtonOption::Animated(background, foreground, foreground, background);
}
/// @brief Create a ButtonOption, using animated colors.
// static
ButtonOption ButtonOption::Animated(Color background,
Color foreground,
Color background_focused,
Color foreground_focused) {
ButtonOption option;
option.transform = [](EntryState s) {
auto element = text(s.label) | borderEmpty;
if (s.focused)
element |= bold;
return element;
};
option.animated_colors.foreground.Set(foreground, foreground_focused);
option.animated_colors.background.Set(background, background_focused);
return option;
}
/// @brief Option for standard Checkbox.
// static
CheckboxOption CheckboxOption::Simple() {
auto option = CheckboxOption();
option.transform = [](EntryState s) {
#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
// Microsoft terminal do not use fonts able to render properly the default
// radiobox glyph.
auto prefix = text(s.state ? "[X] " : "[ ] ");
#else
auto prefix = text(s.state ? "" : "");
#endif
auto t = text(s.label);
if (s.active)
t |= bold;
if (s.focused)
t |= inverted;
return hbox({prefix, t});
};
return option;
}
/// @brief Option for standard Radiobox
// static
RadioboxOption RadioboxOption::Simple() {
auto option = RadioboxOption();
option.transform = [](EntryState s) {
#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
// Microsoft terminal do not use fonts able to render properly the default
// radiobox glyph.
auto prefix = text(s.state ? "(*) " : "( ) ");
#else
auto prefix = text(s.state ? "" : "");
#endif
auto t = text(s.label);
if (s.active)
t |= bold;
if (s.focused)
t |= inverted;
return hbox({prefix, t});
};
return option;
}
} // namespace ftxui
// 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.

View File

@@ -17,8 +17,15 @@ Component Dropdown(ConstStringListRef entries, int* selected) {
Impl(ConstStringListRef entries, int* selected)
: entries_(std::move(entries)), selected_(selected) {
CheckboxOption option;
option.style_checked = "";
option.style_unchecked = "";
option.transform = [](EntryState s) {
auto prefix = text(s.state ? "" : "");
auto t = text(s.label);
if (s.active)
t |= bold;
if (s.focused)
t |= inverted;
return hbox({prefix, t});
};
checkbox_ = Checkbox(&title_, &show_, option),
radiobox_ = Radiobox(entries_, selected_);

View File

@@ -66,7 +66,7 @@ class InputBase : public ComponentBase {
bool hovered = hovered_;
Decorator decorator = dim | main_decorator;
if (is_focused)
decorator = decorator | focus | inverted;
decorator = decorator | focus;
if (hovered || is_focused)
decorator = decorator | inverted;
return text(*placeholder_) | decorator | reflect(box_);

View File

@@ -1,24 +1,65 @@
#include <algorithm> // for max
#include <algorithm> // for max, reverse
#include <chrono> // for milliseconds
#include <functional> // for function
#include <memory> // for shared_ptr, allocator_traits<>::value_type
#include <string> // for operator+, string
#include <utility> // for move
#include <vector> // for vector
#include <memory> // for allocator, shared_ptr, allocator_traits<>::value_type, swap
#include <string> // for char_traits, operator+, string, basic_string
#include <utility> // for move
#include <vector> // for vector, __alloc_traits<>::value_type
#include "ftxui/component/animation.hpp" // for Animator, Linear, Params (ptr only)
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
#include "ftxui/component/component.hpp" // for Make, Menu, MenuEntry
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/component_options.hpp" // for MenuOption, MenuEntryOption
#include "ftxui/component/event.hpp" // for Event, Event::ArrowDown, Event::ArrowUp, Event::End, Event::Home, Event::PageDown, Event::PageUp, Event::Return, Event::Tab, Event::TabReverse
#include "ftxui/component/component.hpp" // for Make, Menu, MenuEntry, Toggle
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/component_options.hpp" // for MenuOption, MenuEntryOption, MenuOption::Direction, UnderlineOption, AnimatedColorOption, AnimatedColorsOption, MenuOption::Down, MenuOption::Left, MenuOption::Right, MenuOption::Up
#include "ftxui/component/event.hpp" // for Event, Event::ArrowDown, Event::ArrowLeft, Event::ArrowRight, Event::ArrowUp, Event::End, Event::Home, Event::PageDown, Event::PageUp, Event::Return, Event::Tab, Event::TabReverse
#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Released, Mouse::WheelDown, Mouse::WheelUp, Mouse::None
#include "ftxui/component/screen_interactive.hpp" // for Component
#include "ftxui/dom/elements.hpp" // for operator|, Element, reflect, text, nothing, select, vbox, Elements, focus
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/util.hpp" // for clamp
#include "ftxui/util/ref.hpp" // for Ref, ConstStringListRef, ConstStringRef
#include "ftxui/dom/elements.hpp" // for operator|, Element, reflect, Decorator, nothing, Elements, bgcolor, color, hbox, separatorHSelector, separatorVSelector, vbox, xflex, yflex, text, bold, focus, inverted, select
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/color.hpp" // for Color
#include "ftxui/screen/util.hpp" // for clamp
#include "ftxui/util/ref.hpp" // for Ref, ConstStringListRef, ConstStringRef
namespace ftxui {
namespace {
Element DefaultOptionTransform(EntryState state) {
state.label = (state.active ? "> " : " ") + state.label;
Element e = text(state.label);
if (state.focused)
e = e | inverted;
if (state.active)
e = e | bold;
return e;
}
bool IsInverted(MenuOption::Direction direction) {
switch (direction) {
case MenuOption::Direction::Up:
case MenuOption::Direction::Left:
return true;
case MenuOption::Direction::Down:
case MenuOption::Direction::Right:
return false;
}
return false; // NOT_REACHED()
}
bool IsHorizontal(MenuOption::Direction direction) {
switch (direction) {
case MenuOption::Direction::Left:
case MenuOption::Direction::Right:
return true;
case MenuOption::Direction::Down:
case MenuOption::Direction::Up:
return false;
}
return false; // NOT_REACHED()
}
} // namespace
/// @brief A list of items. The user can navigate through them.
/// @ingroup component
class MenuBase : public ComponentBase {
@@ -26,26 +67,148 @@ class MenuBase : public ComponentBase {
MenuBase(ConstStringListRef entries, int* selected, Ref<MenuOption> option)
: entries_(entries), selected_(selected), option_(option) {}
bool IsHorizontal() { return ftxui::IsHorizontal(option_->direction); }
void OnChange() {
if (option_->on_change)
option_->on_change();
}
void OnEnter() {
if (option_->on_enter)
option_->on_enter();
}
void Clamp() {
boxes_.resize(size());
*selected_ = util::clamp(*selected_, 0, size() - 1);
focused_entry() = util::clamp(focused_entry(), 0, size() - 1);
}
void OnAnimation(animation::Params& params) override {
animator_first_.OnAnimation(params);
animator_second_.OnAnimation(params);
for (auto& animator : animator_background_)
animator.OnAnimation(params);
for (auto& animator : animator_foreground_)
animator.OnAnimation(params);
}
Element Render() override {
Clamp();
UpdateAnimationTarget();
Elements elements;
bool is_menu_focused = Focused();
if (option_->elements_prefix)
elements.push_back(option_->elements_prefix());
for (int i = 0; i < size(); ++i) {
if (i != 0 && option_->elements_infix)
elements.push_back(option_->elements_infix());
bool is_focused = (focused_entry() == i) && is_menu_focused;
bool is_selected = (*selected_ == i);
auto style = is_selected ? (is_focused ? option_->style_selected_focused
: option_->style_selected)
: (is_focused ? option_->style_focused
: option_->style_normal);
auto focus_management = !is_selected ? nothing
: is_menu_focused ? focus
: select;
auto icon = is_selected ? "> " : " ";
elements.push_back(text(icon + entries_[i]) | style | focus_management |
reflect(boxes_[i]));
: nothing;
EntryState state = {
entries_[i],
false,
is_selected,
is_focused,
};
Element element =
(option_->entries.transform ? option_->entries.transform
: DefaultOptionTransform) //
(std::move(state));
elements.push_back(element | AnimatedColorStyle(i) | reflect(boxes_[i]) |
focus_management);
}
if (option_->elements_postfix)
elements.push_back(option_->elements_postfix());
if (IsInverted(option_->direction))
std::reverse(elements.begin(), elements.end());
Element bar =
IsHorizontal() ? hbox(std::move(elements)) : vbox(std::move(elements));
if (!option_->underline.enabled)
return bar | reflect(box_);
if (IsHorizontal()) {
return vbox({
bar | xflex,
separatorHSelector(first_, second_, //
option_->underline.color_active,
option_->underline.color_inactive),
}) |
reflect(box_);
} else {
return hbox({
separatorVSelector(first_, second_, //
option_->underline.color_active,
option_->underline.color_inactive),
bar | yflex,
}) |
reflect(box_);
}
}
void OnUp() {
switch (option_->direction) {
case MenuOption::Direction::Up:
(*selected_)++;
break;
case MenuOption::Direction::Down:
(*selected_)--;
break;
case MenuOption::Direction::Left:
case MenuOption::Direction::Right:
break;
}
}
void OnDown() {
switch (option_->direction) {
case MenuOption::Direction::Up:
(*selected_)--;
break;
case MenuOption::Direction::Down:
(*selected_)++;
break;
case MenuOption::Direction::Left:
case MenuOption::Direction::Right:
break;
}
}
void OnLeft() {
switch (option_->direction) {
case MenuOption::Direction::Left:
(*selected_)++;
break;
case MenuOption::Direction::Right:
(*selected_)--;
break;
case MenuOption::Direction::Down:
case MenuOption::Direction::Up:
break;
}
}
void OnRight() {
switch (option_->direction) {
case MenuOption::Direction::Left:
(*selected_)--;
break;
case MenuOption::Direction::Right:
(*selected_)++;
break;
case MenuOption::Direction::Down:
case MenuOption::Direction::Up:
break;
}
return vbox(std::move(elements)) | reflect(box_);
}
bool OnEvent(Event event) override {
@@ -59,9 +222,13 @@ class MenuBase : public ComponentBase {
if (Focused()) {
int old_selected = *selected_;
if (event == Event::ArrowUp || event == Event::Character('k'))
(*selected_)--;
OnUp();
if (event == Event::ArrowDown || event == Event::Character('j'))
(*selected_)++;
OnDown();
if (event == Event::ArrowLeft || event == Event::Character('h'))
OnLeft();
if (event == Event::ArrowRight || event == Event::Character('l'))
OnRight();
if (event == Event::PageUp)
(*selected_) -= box_.y_max - box_.y_min;
if (event == Event::PageDown)
@@ -79,13 +246,13 @@ class MenuBase : public ComponentBase {
if (*selected_ != old_selected) {
focused_entry() = *selected_;
option_->on_change();
OnChange();
return true;
}
}
if (event == Event::Return) {
option_->on_enter();
OnEnter();
return true;
}
@@ -114,7 +281,7 @@ class MenuBase : public ComponentBase {
event.mouse().motion == Mouse::Released) {
if (*selected_ != i) {
*selected_ = i;
option_->on_change();
OnChange();
}
return true;
}
@@ -135,19 +302,115 @@ class MenuBase : public ComponentBase {
*selected_ = util::clamp(*selected_, 0, size() - 1);
if (*selected_ != old_selected)
option_->on_change();
OnChange();
return true;
}
void Clamp() {
boxes_.resize(size());
*selected_ = util::clamp(*selected_, 0, size() - 1);
focused_entry() = util::clamp(focused_entry(), 0, size() - 1);
void UpdateAnimationTarget() {
UpdateColorTarget();
UpdateUnderlineTarget();
}
void UpdateColorTarget() {
if (size() != (int)animation_background_.size()) {
animation_background_.resize(size());
animation_foreground_.resize(size());
animator_background_.clear();
animator_foreground_.clear();
for (int i = 0; i < size(); ++i) {
animation_background_[i] = 0.f;
animation_foreground_[i] = 0.f;
animator_background_.emplace_back(&animation_background_[i], 0.f,
std::chrono::milliseconds(0),
animation::easing::Linear);
animator_foreground_.emplace_back(&animation_foreground_[i], 0.f,
std::chrono::milliseconds(0),
animation::easing::Linear);
}
}
bool is_menu_focused = Focused();
for (int i = 0; i < size(); ++i) {
bool is_focused = (focused_entry() == i) && is_menu_focused;
bool is_selected = (*selected_ == i);
float target = is_selected ? 1.f : is_focused ? 0.5f : 0.f;
if (animator_background_[i].to() != target) {
animator_background_[i] = animation::Animator(
&animation_background_[i], target,
option_->entries.animated_colors.background.duration,
option_->entries.animated_colors.background.function);
animator_foreground_[i] = animation::Animator(
&animation_foreground_[i], target,
option_->entries.animated_colors.foreground.duration,
option_->entries.animated_colors.foreground.function);
}
}
}
Decorator AnimatedColorStyle(int i) {
Decorator style = nothing;
if (option_->entries.animated_colors.foreground.enabled) {
style = style | color(Color::Interpolate(
animation_foreground_[i],
option_->entries.animated_colors.foreground.inactive,
option_->entries.animated_colors.foreground.active));
}
if (option_->entries.animated_colors.background.enabled) {
style = style | bgcolor(Color::Interpolate(
animation_background_[i],
option_->entries.animated_colors.background.inactive,
option_->entries.animated_colors.background.active));
}
return style;
}
void UpdateUnderlineTarget() {
if (!option_->underline.enabled)
return;
if (FirstTarget() == animator_first_.to() &&
SecondTarget() == animator_second_.to()) {
return;
}
if (FirstTarget() >= animator_first_.to()) {
animator_first_ = animation::Animator(
&first_, FirstTarget(), option_->underline.follower_duration,
option_->underline.follower_function,
option_->underline.follower_delay);
animator_second_ = animation::Animator(
&second_, SecondTarget(), option_->underline.leader_duration,
option_->underline.leader_function, option_->underline.leader_delay);
} else {
animator_first_ = animation::Animator(
&first_, FirstTarget(), option_->underline.leader_duration,
option_->underline.leader_function, option_->underline.leader_delay);
animator_second_ = animation::Animator(
&second_, SecondTarget(), option_->underline.follower_duration,
option_->underline.follower_function,
option_->underline.follower_delay);
}
}
bool Focusable() const final { return entries_.size(); }
int& focused_entry() { return option_->focused_entry(); }
int size() const { return entries_.size(); }
int FirstTarget() {
if (boxes_.size() == 0)
return 0;
return IsHorizontal() ? boxes_[*selected_].x_min - box_.x_min
: boxes_[*selected_].y_min - box_.y_min;
}
int SecondTarget() {
if (boxes_.size() == 0)
return 0;
return IsHorizontal() ? boxes_[*selected_].x_max - box_.x_min
: boxes_[*selected_].y_max - box_.y_min;
}
protected:
ConstStringListRef entries_;
@@ -156,6 +419,16 @@ class MenuBase : public ComponentBase {
std::vector<Box> boxes_;
Box box_;
float first_ = 0.f;
float second_ = 0.f;
animation::Animator animator_first_ = animation::Animator(&first_, 0.f);
animation::Animator animator_second_ = animation::Animator(&second_, 0.f);
std::vector<animation::Animator> animator_background_;
std::vector<animation::Animator> animator_foreground_;
std::vector<float> animation_background_;
std::vector<float> animation_foreground_;
};
/// @brief A list of text. The focused element is selected.
@@ -163,7 +436,6 @@ class MenuBase : public ComponentBase {
/// @param selected The index of the currently selected element.
/// @param option Additional optional parameters.
/// @ingroup component
/// @see MenuBase
///
/// ### Example
///
@@ -192,6 +464,41 @@ Component Menu(ConstStringListRef entries,
return Make<MenuBase>(entries, selected, std::move(option));
}
/// @brief An horizontal list of elements. The user can navigate through them.
/// @param entries The list of selectable entries to display.
/// @param selected Reference the selected entry.
/// @param See also |Menu|.
/// @ingroup component
Component Toggle(ConstStringListRef entries, int* selected) {
return Menu(entries, selected, MenuOption::Toggle());
}
/// @brief A specific menu entry. They can be put into a Container::Vertical to
/// form a menu.
/// @param label The text drawn representing this element.
/// @param option Additional optional parameters.
/// @ingroup component
///
/// ### Example
///
/// ```cpp
/// auto screen = ScreenInteractive::TerminalOutput();
/// int selected = 0;
/// auto menu = Container::Vertical({
/// MenuEntry("entry 1"),
/// MenuEntry("entry 2"),
/// MenuEntry("entry 3"),
/// }, &selected);
/// screen.Loop(menu);
/// ```
///
/// ### Output
///
/// ```bash
/// > entry 1
/// entry 2
/// entry 3
/// ```
Component MenuEntry(ConstStringRef label, Ref<MenuEntryOption> option) {
class Impl : public ComponentBase {
public:
@@ -201,15 +508,56 @@ Component MenuEntry(ConstStringRef label, Ref<MenuEntryOption> option) {
private:
Element Render() override {
bool focused = Focused();
auto style =
hovered_ ? (focused ? option_->style_selected_focused
: option_->style_selected)
: (focused ? option_->style_focused : option_->style_normal);
UpdateAnimationTarget();
EntryState state = {
*label_,
hovered_,
focused,
false,
};
Element element =
(option_->transform ? option_->transform : DefaultOptionTransform) //
(std::move(state));
auto focus_management = focused ? select : nothing;
auto label = focused ? "> " + (*label_) //
: " " + (*label_);
return text(label) | style | focus_management | reflect(box_);
return element | AnimatedColorStyle() | focus_management | reflect(box_);
}
void UpdateAnimationTarget() {
bool focused = Focused();
float target = focused ? 1.0f : hovered_ ? 0.5f : 0.0f;
if (target == animator_background_.to())
return;
animator_background_ =
animation::Animator(&animation_background_, target,
option_->animated_colors.background.duration,
option_->animated_colors.background.function);
animator_foreground_ =
animation::Animator(&animation_foreground_, target,
option_->animated_colors.foreground.duration,
option_->animated_colors.foreground.function);
}
Decorator AnimatedColorStyle() {
Decorator style = nothing;
if (option_->animated_colors.foreground.enabled) {
style = style | color(Color::Interpolate(
animation_foreground_,
option_->animated_colors.foreground.inactive,
option_->animated_colors.foreground.active));
}
if (option_->animated_colors.background.enabled) {
style = style | bgcolor(Color::Interpolate(
animation_background_,
option_->animated_colors.background.inactive,
option_->animated_colors.background.active));
}
return style;
}
bool Focusable() const override { return true; }
bool OnEvent(Event event) override {
if (!event.is_mouse())
@@ -228,10 +576,23 @@ Component MenuEntry(ConstStringRef label, Ref<MenuEntryOption> option) {
return false;
}
void OnAnimation(animation::Params& params) override {
animator_background_.OnAnimation(params);
animator_foreground_.OnAnimation(params);
}
ConstStringRef label_;
Ref<MenuEntryOption> option_;
Box box_;
bool hovered_ = false;
float animation_background_ = 0.f;
float animation_foreground_ = 0.f;
animation::Animator animator_background_ =
animation::Animator(&animation_background_, 0.f);
animation::Animator animator_foreground_ =
animation::Animator(&animation_foreground_, 0.f);
};
return Make<Impl>(std::move(label), std::move(option));

View File

@@ -29,14 +29,6 @@ class RadioboxBase : public ComponentBase {
int* selected,
Ref<RadioboxOption> option)
: entries_(entries), selected_(selected), option_(std::move(option)) {
#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
// Microsoft terminal do not use fonts able to render properly the default
// radiobox glyph.
if (option_->style_checked == "")
option_->style_checked = "(*)";
if (option_->style_unchecked == "")
option_->style_unchecked = "( )";
#endif
hovered_ = *selected_;
}
@@ -48,19 +40,21 @@ class RadioboxBase : public ComponentBase {
for (int i = 0; i < size(); ++i) {
bool is_focused = (focused_entry() == i) && is_menu_focused;
bool is_selected = (hovered_ == i);
auto style = is_selected ? (is_focused ? option_->style_selected_focused
: option_->style_selected)
: (is_focused ? option_->style_focused
: option_->style_normal);
auto focus_management = !is_selected ? nothing
: is_menu_focused ? focus
: select;
auto state = EntryState{
entries_[i],
*selected_ == i,
is_selected,
is_focused,
};
auto element =
(option_->transform
? option_->transform
: RadioboxOption::Simple().transform)(std::move(state));
const std::string& symbol =
*selected_ == i ? option_->style_checked : option_->style_unchecked;
elements.push_back(hbox(text(symbol), text(entries_[i]) | style) |
focus_management | reflect(boxes_[i]));
elements.push_back(element | focus_management | reflect(boxes_[i]));
}
return vbox(std::move(elements)) | reflect(box_);
}

View File

@@ -1,21 +1,23 @@
#include <stdio.h> // for fileno, stdin
#include <algorithm> // for copy, max, min
#include <chrono> // for operator-, duration, operator>=, milliseconds, time_point, common_type<>::type
#include <csignal> // for signal, raise, SIGTSTP, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIGWINCH
#include <cstdlib> // for NULL
#include <functional> // for function
#include <initializer_list> // for initializer_list
#include <iostream> // for cout, ostream, basic_ostream, operator<<, endl, flush
#include <stack> // for stack
#include <thread> // for thread
#include <thread> // for thread, sleep_for
#include <type_traits> // for decay_t
#include <utility> // for swap, move
#include <variant> // for visit
#include <vector> // for vector
#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/event.hpp" // for Event
#include "ftxui/component/receiver.hpp" // for ReceiverImpl, MakeReceiver, Sender, SenderImpl, Receiver
#include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver
#include "ftxui/component/screen_interactive.hpp"
#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
#include "ftxui/dom/node.hpp" // for Node, Render
@@ -45,6 +47,14 @@
namespace ftxui {
namespace animation {
void RequestAnimationFrame() {
auto* screen = ScreenInteractive::Active();
if (screen)
screen->RequestAnimationFrame();
}
} // namespace animation
namespace {
ScreenInteractive* g_active_screen = nullptr;
@@ -236,6 +246,13 @@ class CapturedMouseImpl : public CapturedMouseInterface {
std::function<void(void)> callback_;
};
void AnimationListener(std::atomic<bool>* quit, Sender<Task> out) {
while (!*quit) {
out->Send(AnimationTask());
std::this_thread::sleep_for(std::chrono::milliseconds(15));
}
}
} // namespace
ScreenInteractive::ScreenInteractive(int dimx,
@@ -276,6 +293,15 @@ void ScreenInteractive::PostEvent(Event event) {
Post(event);
}
void ScreenInteractive::RequestAnimationFrame() {
if (animation_requested_)
return;
animation_requested_ = true;
auto now = animation::Clock::now();
if (now - previous_animation_time >= std::chrono::milliseconds(33))
previous_animation_time = now;
}
CapturedMouse ScreenInteractive::CaptureMouse() {
if (mouse_captured)
return nullptr;
@@ -330,6 +356,11 @@ Closure ScreenInteractive::WithRestoredIO(Closure fn) {
};
}
// static
ScreenInteractive* ScreenInteractive::Active() {
return g_active_screen;
}
void ScreenInteractive::Install() {
// After uninstalling the new configuration, flush it to the terminal to
// ensure it is fully applied:
@@ -432,54 +463,85 @@ void ScreenInteractive::Install() {
task_sender_ = task_receiver_->MakeSender();
event_listener_ =
std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
animation_listener_ =
std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
}
void ScreenInteractive::Uninstall() {
ExitLoopClosure()();
event_listener_.join();
animation_listener_.join();
OnExit(0);
}
void ScreenInteractive::Main(Component component) {
previous_animation_time = animation::Clock::now();
auto draw = [&] {
Draw(component);
std::cout << ToString() << set_cursor_position;
Flush();
Clear();
};
draw();
while (!quit_) {
if (!task_receiver_->HasPending()) {
Draw(component);
std::cout << ToString() << set_cursor_position;
Flush();
Clear();
}
if (!task_receiver_->HasPending())
draw();
Task task;
if (!task_receiver_->Receive(&task))
break;
bool continue_event_loop = true;
while (continue_event_loop) {
continue_event_loop = false;
Task task;
if (!task_receiver_->Receive(&task))
break;
std::visit(
[&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
std::visit(
[&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
// Handle Event.
if constexpr (std::is_same_v<T, Event>) {
if (arg.is_cursor_reporting()) {
cursor_x_ = arg.cursor_x();
cursor_y_ = arg.cursor_y();
// Handle Event.
if constexpr (std::is_same_v<T, Event>) {
if (arg.is_cursor_reporting()) {
cursor_x_ = arg.cursor_x();
cursor_y_ = arg.cursor_y();
return;
}
if (arg.is_mouse()) {
arg.mouse().x -= cursor_x_;
arg.mouse().y -= cursor_y_;
}
arg.screen_ = this;
component->OnEvent(arg);
}
// Handle callback
if constexpr (std::is_same_v<T, Closure>) {
arg();
return;
}
if (arg.is_mouse()) {
arg.mouse().x -= cursor_x_;
arg.mouse().y -= cursor_y_;
// Handle Animation
if constexpr (std::is_same_v<T, AnimationTask>) {
if (!animation_requested_) {
continue_event_loop = true;
return;
}
animation_requested_ = false;
animation::TimePoint now = animation::Clock::now();
animation::Duration delta = now - previous_animation_time;
previous_animation_time = now;
animation::Params params(delta);
component->OnAnimation(params);
}
arg.screen_ = this;
component->OnEvent(arg);
}
// Handle callback
if constexpr (std::is_same_v<T, Closure>)
arg();
},
task);
},
task);
}
}
}

View File

@@ -1,144 +0,0 @@
#include <algorithm> // for max
#include <functional> // for function
#include <memory> // for shared_ptr, allocator_traits<>::value_type
#include <utility> // for move
#include <vector> // for vector
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
#include "ftxui/component/component.hpp" // for Make, Toggle
#include "ftxui/component/component_base.hpp" // for Component, ComponentBase
#include "ftxui/component/component_options.hpp" // for ToggleOption
#include "ftxui/component/event.hpp" // for Event, Event::ArrowLeft, Event::ArrowRight, Event::Return, Event::Tab, Event::TabReverse
#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Pressed
#include "ftxui/dom/elements.hpp" // for operator|, Element, Elements, hbox, reflect, separator, text, focus, nothing, select
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/util.hpp" // for clamp
#include "ftxui/util/ref.hpp" // for Ref, ConstStringListRef
namespace ftxui {
namespace {
/// @brief An horizontal list of elements. The user can navigate through them.
/// @ingroup component
class ToggleBase : public ComponentBase {
public:
ToggleBase(ConstStringListRef entries,
int* selected,
Ref<ToggleOption> option)
: entries_(entries), selected_(selected), option_(std::move(option)) {}
private:
Element Render() override {
Clamp();
Elements children;
bool is_toggle_focused = Focused();
for (int i = 0; i < size(); ++i) {
// Separator.
if (i != 0)
children.push_back(separator());
bool is_focused = (focused_entry() == i) && is_toggle_focused;
bool is_selected = (*selected_ == i);
auto style = is_selected ? (is_focused ? option_->style_selected_focused
: option_->style_selected)
: (is_focused ? option_->style_focused
: option_->style_normal);
auto focus_management = !is_selected ? nothing
: is_toggle_focused ? focus
: select;
children.push_back(text(entries_[i]) | style | focus_management |
reflect(boxes_[i]));
}
return hbox(std::move(children));
}
bool OnEvent(Event event) override {
Clamp();
if (event.is_mouse())
return OnMouseEvent(event);
int old_selected = *selected_;
if (event == Event::ArrowLeft || event == Event::Character('h'))
(*selected_)--;
if (event == Event::ArrowRight || event == Event::Character('l'))
(*selected_)++;
if (event == Event::Tab && size())
*selected_ = (*selected_ + 1) % size();
if (event == Event::TabReverse && size())
*selected_ = (*selected_ + size() - 1) % size();
*selected_ = util::clamp(*selected_, 0, size() - 1);
if (old_selected != *selected_) {
focused_entry() = *selected_;
option_->on_change();
return true;
}
if (event == Event::Return) {
option_->on_enter();
return true;
}
return false;
}
bool OnMouseEvent(Event event) {
if (!CaptureMouse(event))
return false;
for (int i = 0; i < size(); ++i) {
if (!boxes_[i].Contain(event.mouse().x, event.mouse().y))
continue;
TakeFocus();
focused_entry() = i;
if (event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Pressed) {
TakeFocus();
if (*selected_ != i) {
*selected_ = i;
option_->on_change();
}
return true;
}
}
return false;
}
void Clamp() {
boxes_.resize(size());
*selected_ = util::clamp(*selected_, 0, size() - 1);
focused_entry() = util::clamp(focused_entry(), 0, size() - 1);
}
bool Focusable() const final { return size(); }
int& focused_entry() { return option_->focused_entry(); }
int size() const { return entries_.size(); }
ConstStringListRef entries_;
int* selected_ = 0;
std::vector<Box> boxes_;
Ref<ToggleOption> option_;
};
} // namespace
/// @brief An horizontal list of elements. The user can navigate through them.
/// @param entries The list of selectable entries to display.
/// @param selected Reference the selected entry.
/// @param option Additional optional parameters.
/// @ingroup component
Component Toggle(ConstStringListRef entries,
int* selected,
Ref<ToggleOption> option) {
return Make<ToggleBase>(entries, selected, std::move(option));
}
} // namespace ftxui
// 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.

View File

@@ -90,10 +90,10 @@ TEST(ToggleTest, OnChange) {
std::vector<std::string> entries = {"1", "2", "3"};
int selected = 0;
int counter = 0;
auto option = ToggleOption();
auto option = MenuOption::Toggle();
option.on_change = [&] { counter++; };
auto toggle = Toggle(&entries, &selected, &option);
auto toggle = Menu(&entries, &selected, &option);
EXPECT_FALSE(toggle->OnEvent(Event::ArrowLeft)); // Reached far left.
EXPECT_EQ(counter, 0);
@@ -120,9 +120,9 @@ TEST(ToggleTest, OnEnter) {
int selected = 0;
int counter = 0;
auto option = ToggleOption();
auto option = MenuOption::Toggle();
option.on_enter = [&] { counter++; };
auto toggle = Toggle(&entries, &selected, &option);
auto toggle = Menu(&entries, &selected, &option);
EXPECT_FALSE(toggle->OnEvent(Event::ArrowLeft)); // Reached far left.
EXPECT_TRUE(toggle->OnEvent(Event::Return));
@@ -155,9 +155,9 @@ TEST(ToggleTest, RemoveEntries) {
int focused_entry = 0;
int selected = 0;
std::vector<std::string> entries = {"1", "2", "3"};
ToggleOption option;
auto option = MenuOption::Toggle();
option.focused_entry = &focused_entry;
auto toggle = Toggle(&entries, &selected, option);
auto toggle = Menu(&entries, &selected, option);
EXPECT_EQ(selected, 0);
EXPECT_EQ(focused_entry, 0);

View File

@@ -1,9 +1,7 @@
#include <functional> // for function
#include <memory> // for __shared_ptr_access
#include "ftxui/component/component.hpp" // for ElementDecorator, Renderer, ComponentDecorator, operator|, operator|=
#include "ftxui/component/component_base.hpp" // for Component, ComponentBase
#include "ftxui/dom/elements.hpp" // for Element
#include "ftxui/component/component.hpp" // for Renderer, ComponentDecorator, ElementDecorator, operator|, operator|=
#include "ftxui/component/component_base.hpp" // for Component
namespace ftxui {

View File

@@ -1,10 +1,11 @@
#include <memory> // for make_shared
#include <memory> // for make_shared, allocator
#include <string> // for string
#include "ftxui/dom/elements.hpp" // for Element, separator
#include "ftxui/dom/node.hpp" // for Node
#include "ftxui/dom/elements.hpp" // for Element, BorderStyle, LIGHT, separator, DOUBLE, EMPTY, HEAVY, separatorCharacter, separatorDouble, separatorEmpty, separatorHSelector, separatorHeavy, separatorLight, separatorStyled, separatorVSelector
#include "ftxui/dom/node.hpp" // for Node
#include "ftxui/dom/requirement.hpp" // for Requirement
#include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/color.hpp" // for Color
#include "ftxui/screen/screen.hpp" // for Pixel, Screen
namespace ftxui {
@@ -369,6 +370,142 @@ Element separator(Pixel pixel) {
return std::make_shared<SeparatorWithPixel>(pixel);
}
/// @brief Draw an horizontal bar, with the area in between left/right colored
/// differently.
/// @param left the left limit of the active area.
/// @param right the right limit of the active area.
/// @param selected_color the color of the selected area.
/// @param unselected_color the color of the unselected area.
///
/// ### Example
///
/// ```cpp
/// Element document = separatorHSelector(2,5, Color::White, Color::Blue);
/// ```
Element separatorHSelector(float left,
float right,
Color selected_color,
Color unselected_color) {
class Impl : public Node {
public:
Impl(float left, float right, Color selected_color, Color unselected_color)
: left_(left),
right_(right),
selected_color_(selected_color),
unselected_color_(unselected_color) {}
void ComputeRequirement() override {
requirement_.min_x = 1;
requirement_.min_y = 1;
}
void Render(Screen& screen) override {
if (box_.y_max < box_.y_min)
return;
// This are the two location with an empty demi-cell.
int demi_cell_left = left_ * 2 - 1;
int demi_cell_right = right_ * 2 + 2;
int y = box_.y_min;
for (int x = box_.x_min; x <= box_.x_max; ++x) {
Pixel& pixel = screen.PixelAt(x, y);
int a = (x - box_.x_min) * 2;
int b = a + 1;
bool a_empty = demi_cell_left == a || demi_cell_right == a;
bool b_empty = demi_cell_left == b || demi_cell_right == b;
if (!a_empty && !b_empty) {
pixel.character = "";
pixel.automerge = true;
} else {
pixel.character = a_empty ? "" : "";
pixel.automerge = false;
}
if (demi_cell_left <= a && b <= demi_cell_right)
pixel.foreground_color = selected_color_;
else
pixel.foreground_color = unselected_color_;
}
}
float left_;
float right_;
Color selected_color_;
Color unselected_color_;
};
return std::make_shared<Impl>(left, right, selected_color, unselected_color);
}
/// @brief Draw an vertical bar, with the area in between up/downcolored
/// differently.
/// @param up the left limit of the active area.
/// @param down the right limit of the active area.
/// @param selected_color the color of the selected area.
/// @param unselected_color the color of the unselected area.
///
/// ### Example
///
/// ```cpp
/// Element document = separatorHSelector(2,5, Color::White, Color::Blue);
/// ```
Element separatorVSelector(float up,
float down,
Color selected_color,
Color unselected_color) {
class Impl : public Node {
public:
Impl(float up, float down, Color selected_color, Color unselected_color)
: up_(up),
down_(down),
selected_color_(selected_color),
unselected_color_(unselected_color) {}
void ComputeRequirement() override {
requirement_.min_x = 1;
requirement_.min_y = 1;
}
void Render(Screen& screen) override {
if (box_.x_max < box_.x_min)
return;
// This are the two location with an empty demi-cell.
int demi_cell_up = up_ * 2 - 1;
int demi_cell_down = down_ * 2 + 2;
int x = box_.x_min;
for (int y = box_.y_min; y <= box_.y_max; ++y) {
Pixel& pixel = screen.PixelAt(x, y);
int a = (y - box_.y_min) * 2;
int b = a + 1;
bool a_empty = demi_cell_up == a || demi_cell_down == a;
bool b_empty = demi_cell_up == b || demi_cell_down == b;
if (!a_empty && !b_empty) {
pixel.character = "";
pixel.automerge = true;
} else {
pixel.character = a_empty ? "" : "";
pixel.automerge = false;
}
if (demi_cell_up <= a && b <= demi_cell_down)
pixel.foreground_color = selected_color_;
else
pixel.foreground_color = unselected_color_;
}
}
float up_;
float down_;
Color selected_color_;
Color unselected_color_;
};
return std::make_shared<Impl>(up, down, selected_color, unselected_color);
}
} // namespace ftxui
// Copyright 2020 Arthur Sonzogni. All rights reserved.

View File

@@ -145,6 +145,77 @@ Color Color::HSV(uint8_t h, uint8_t s, uint8_t v) {
return Color(0, 0, 0);
}
// static
Color Color::Interpolate(float t, const Color& a, const Color& b) {
float red;
float green;
float blue;
switch (a.type_) {
case ColorType::Palette1: {
if (t < 0.5)
return a;
else
return b;
}
case ColorType::Palette16: {
ColorInfo info = GetColorInfo(Color::Palette16(a.index_));
red = info.red * (1 - t);
green = info.green * (1 - t);
blue = info.blue * (1 - t);
break;
}
case ColorType::Palette256: {
ColorInfo info = GetColorInfo(Color::Palette256(a.index_));
red = info.red * (1 - t);
green = info.green * (1 - t);
blue = info.blue * (1 - t);
break;
}
case ColorType::TrueColor: {
red = a.red_ * (1 - t);
green = a.green_ * (1 - t);
blue = a.blue_ * (1 - t);
break;
}
}
switch (b.type_) {
case ColorType::Palette1: {
if (t > 0.5)
return a;
else
return b;
}
case ColorType::Palette16: {
ColorInfo info = GetColorInfo(Color::Palette16(b.index_));
red += info.red * t;
green += info.green * t;
blue += info.blue * t;
break;
}
case ColorType::Palette256: {
ColorInfo info = GetColorInfo(Color::Palette256(b.index_));
red += info.red * t;
green += info.green * t;
blue += info.blue * t;
break;
}
case ColorType::TrueColor: {
red += b.red_ * t;
green += b.green_ * t;
blue += b.blue_ * t;
break;
}
}
return Color::RGB(red, green, blue);
}
inline namespace literals {
Color operator""_rgb(unsigned long long int combined) {