From b0e087ecefbdb8ef4cb0f6a5335e9a731c3c707c Mon Sep 17 00:00:00 2001 From: Ayaan <138162656+horrifyingHorse@users.noreply.github.com> Date: Wed, 19 Mar 2025 20:03:05 +0530 Subject: [PATCH] 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 --- CHANGELOG.md | 2 + examples/component/CMakeLists.txt | 3 +- examples/component/checkbox.cpp | 44 ++++---- examples/component/input_in_frame.cpp | 30 ++++++ include/ftxui/component/component_base.hpp | 6 +- include/ftxui/dom/elements.hpp | 2 +- include/ftxui/dom/node.hpp | 2 + include/ftxui/dom/requirement.hpp | 29 +++-- include/ftxui/screen/box.hpp | 1 + src/ftxui/component/button.cpp | 8 +- src/ftxui/component/checkbox.cpp | 8 +- src/ftxui/component/component.cpp | 41 +++++++- src/ftxui/component/container.cpp | 12 +-- src/ftxui/component/dropdown.cpp | 17 +-- src/ftxui/component/hoverable.cpp | 8 +- src/ftxui/component/input.cpp | 11 +- src/ftxui/component/maybe.cpp | 4 +- src/ftxui/component/menu.cpp | 40 +++---- src/ftxui/component/modal.cpp | 2 +- src/ftxui/component/radiobox.cpp | 13 ++- src/ftxui/component/renderer.cpp | 4 +- src/ftxui/component/resizable_split.cpp | 2 +- src/ftxui/component/slider.cpp | 117 +++++++++++---------- src/ftxui/component/slider_test.cpp | 42 ++++---- src/ftxui/component/window.cpp | 2 +- src/ftxui/dom/border.cpp | 17 ++- src/ftxui/dom/box_helper.hpp | 2 +- src/ftxui/dom/dbox.cpp | 21 ++-- src/ftxui/dom/flexbox.cpp | 26 ++--- src/ftxui/dom/focus.cpp | 17 ++- src/ftxui/dom/frame.cpp | 106 +++++-------------- src/ftxui/dom/gridbox.cpp | 22 ++-- src/ftxui/dom/gridbox_test.cpp | 2 +- src/ftxui/dom/hbox.cpp | 22 ++-- src/ftxui/dom/node.cpp | 52 ++++++++- src/ftxui/dom/scroll_indicator_test.cpp | 2 +- src/ftxui/dom/vbox.cpp | 22 ++-- src/ftxui/screen/box.cpp | 11 ++ 38 files changed, 431 insertions(+), 341 deletions(-) create mode 100644 examples/component/input_in_frame.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b607ece..69f2e231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,8 @@ current (development) - See `selectionForegroundColor` decorator. - See `selectionStyle(style)` decorator. - See `selectionStyleReset` decorator. +- Breaking change: Change how "focus"/"select" are handled. This fixes the + behavior. ### Screen - Feature: Add `Box::IsEmpty()`. diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt index 62318b03..3444d563 100644 --- a/examples/component/CMakeLists.txt +++ b/examples/component/CMakeLists.txt @@ -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,8 +39,8 @@ example(radiobox) example(radiobox_in_frame) example(renderer) example(resizable_split) -example(selection) example(scrollbar) +example(selection) example(slider) example(slider_direction) example(slider_rgb) diff --git a/examples/component/checkbox.cpp b/examples/component/checkbox.cpp index 803f110e..6fcd6d5b 100644 --- a/examples/component/checkbox.cpp +++ b/examples/component/checkbox.cpp @@ -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 // for allocator, __shared_ptr_access -#include // for string, basic_string, operator+, to_string -#include // for vector +#include // for array +#include +#include // for shared_ptr, __shared_ptr_access +#include // 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 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; } diff --git a/examples/component/input_in_frame.cpp b/examples/component/input_in_frame.cpp new file mode 100644 index 00000000..803f110e --- /dev/null +++ b/examples/component/input_in_frame.cpp @@ -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 // for allocator, __shared_ptr_access +#include // for string, basic_string, operator+, to_string +#include // 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 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); +} diff --git a/include/ftxui/component/component_base.hpp b/include/ftxui/component/component_base.hpp index ef1b751c..fcc67e16 100644 --- a/include/ftxui/component/component_base.hpp +++ b/include/ftxui/component/component_base.hpp @@ -50,7 +50,10 @@ class ComponentBase { 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. @@ -94,6 +97,7 @@ class ComponentBase { private: ComponentBase* parent_ = nullptr; + bool in_render = false; }; } // namespace ftxui diff --git a/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp index fa16080e..fe778e32 100644 --- a/include/ftxui/dom/elements.hpp +++ b/include/ftxui/dom/elements.hpp @@ -161,7 +161,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. diff --git a/include/ftxui/dom/node.hpp b/include/ftxui/dom/node.hpp index 87edce77..6357ee65 100644 --- a/include/ftxui/dom/node.hpp +++ b/include/ftxui/dom/node.hpp @@ -59,6 +59,8 @@ class Node { }; virtual void Check(Status* status); + friend void Render(Screen& screen, Node* node, Selection& selection); + protected: Elements children_; Requirement requirement_; diff --git a/include/ftxui/dom/requirement.hpp b/include/ftxui/dom/requirement.hpp index 1b0a8842..b8a5c573 100644 --- a/include/ftxui/dom/requirement.hpp +++ b/include/ftxui/dom/requirement.hpp @@ -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 diff --git a/include/ftxui/screen/box.hpp b/include/ftxui/screen/box.hpp index 37708038..128362d9 100644 --- a/include/ftxui/screen/box.hpp +++ b/include/ftxui/screen/box.hpp @@ -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; diff --git a/src/ftxui/component/button.cpp b/src/ftxui/component/button.cpp index 844dd396..79ad78ac 100644 --- a/src/ftxui/component/button.cpp +++ b/src/ftxui/component/button.cpp @@ -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,14 +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, 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() { diff --git a/src/ftxui/component/checkbox.cpp b/src/ftxui/component/checkbox.cpp index b4b7f158..41a4e957 100644 --- a/src/ftxui/component/checkbox.cpp +++ b/src/ftxui/component/checkbox.cpp @@ -23,16 +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_, -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 { @@ -69,7 +70,6 @@ class CheckboxBase : public ComponentBase, public CheckboxOption { event.mouse().motion == Mouse::Pressed) { *checked = !*checked; on_change(); - TakeFocus(); return true; } diff --git a/src/ftxui/component/component.cpp b/src/ftxui/component/component.cpp index e0c38dd4..e4177d67 100644 --- a/src/ftxui/component/component.cpp +++ b/src/ftxui/component/component.cpp @@ -15,6 +15,7 @@ #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 namespace ftxui::animation { class Params; @@ -103,10 +104,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(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(); } diff --git a/src/ftxui/component/container.cpp b/src/ftxui/component/container.cpp index 410bb9f5..3e32c744 100644 --- a/src/ftxui/component/container.cpp +++ b/src/ftxui/component/container.cpp @@ -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_) { @@ -182,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_) { @@ -218,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(); @@ -244,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()); @@ -334,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(std::move(children), selector); @@ -355,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); diff --git a/src/ftxui/component/dropdown.cpp b/src/ftxui/component/dropdown.cpp index 690a6e2d..1635f5be 100644 --- a/src/ftxui/component/dropdown.cpp +++ b/src/ftxui/component/dropdown.cpp @@ -44,12 +44,12 @@ Component Dropdown(DropdownOption option) { })); } - Element Render() override { - radiobox.selected = + Element OnRender() override { + selected_ = util::clamp(radiobox.selected(), 0, int(radiobox.entries.size()) - 1); selected_ = util::clamp(selected_(), 0, int(radiobox.entries.size()) - 1); - if (selected_() >= 0) { + if (selected_() >= 0 && selected_() < int(radiobox.entries.size())) { title_ = radiobox.entries[selected_()]; } @@ -70,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(); diff --git a/src/ftxui/component/hoverable.cpp b/src/ftxui/component/hoverable.cpp index 85c8e0f2..9af6498b 100644 --- a/src/ftxui/component/hoverable.cpp +++ b/src/ftxui/component/hoverable.cpp @@ -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 { diff --git a/src/ftxui/component/input.cpp b/src/ftxui/component/input.cpp index 8f5d728c..a61e6256 100644 --- a/src/ftxui/component/input.cpp +++ b/src/ftxui/component/input.cpp @@ -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 diff --git a/src/ftxui/component/maybe.cpp b/src/ftxui/component/maybe.cpp index 8a05053d..2dd462e7 100644 --- a/src/ftxui/component/maybe.cpp +++ b/src/ftxui/component/maybe.cpp @@ -24,8 +24,8 @@ Component Maybe(Component child, std::function show) { explicit Impl(std::function show) : show_(std::move(show)) {} private: - Element Render() override { - return show_() ? ComponentBase::Render() : std::make_unique(); + Element OnRender() override { + return show_() ? ComponentBase::OnRender() : std::make_unique(); } bool Focusable() const override { return show_() && ComponentBase::Focusable(); diff --git a/src/ftxui/component/menu.cpp b/src/ftxui/component/menu.cpp index 223d3783..ae9d2483 100644 --- a/src/ftxui/component/menu.cpp +++ b/src/ftxui/component/menu.cpp @@ -105,7 +105,7 @@ class MenuBase : public ComponentBase, public MenuOption { } } - Element Render() override { + Element OnRender() override { Clamp(); UpdateAnimationTarget(); @@ -126,16 +126,15 @@ class MenuBase : public ComponentBase, public MenuOption { 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()); @@ -145,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_); @@ -618,20 +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, Index(), + 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() { diff --git a/src/ftxui/component/modal.cpp b/src/ftxui/component/modal.cpp index 1c970c39..213e9e56 100644 --- a/src/ftxui/component/modal.cpp +++ b/src/ftxui/component/modal.cpp @@ -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_) { diff --git a/src/ftxui/component/radiobox.cpp b/src/ftxui/component/radiobox.cpp index 4c823ded..f1b1d477 100644 --- a/src/ftxui/component/radiobox.cpp +++ b/src/ftxui/component/radiobox.cpp @@ -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,18 +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, 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) diff --git a/src/ftxui/component/renderer.cpp b/src/ftxui/component/renderer.cpp index 8754839e..8ca47839 100644 --- a/src/ftxui/component/renderer.cpp +++ b/src/ftxui/component/renderer.cpp @@ -31,7 +31,7 @@ Component Renderer(std::function render) { public: explicit Impl(std::function render) : render_(std::move(render)) {} - Element Render() override { return render_(); } + Element OnRender() override { return render_(); } std::function render_; }; @@ -88,7 +88,7 @@ Component Renderer(std::function 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)) { diff --git a/src/ftxui/component/resizable_split.cpp b/src/ftxui/component/resizable_split.cpp index 33cbb609..39848f65 100644 --- a/src/ftxui/component/resizable_split.cpp +++ b/src/ftxui/component/resizable_split.cpp @@ -94,7 +94,7 @@ class ResizableSplitBase : public ComponentBase { return false; } - Element Render() final { + Element OnRender() final { switch (options_->direction()) { case Direction::Left: return RenderLeft(); diff --git a/src/ftxui/component/slider.cpp b/src/ftxui/component/slider.cpp index e8efbb85..0815c6b8 100644 --- a/src/ftxui/component/slider.cpp +++ b/src/ftxui/component/slider.cpp @@ -39,7 +39,7 @@ class SliderBase : public SliderOption, public ComponentBase { public: explicit SliderBase(SliderOption options) : SliderOption(options) {} - Element Render() override { + Element OnRender() override { auto gauge_color = Focused() ? color(this->color_active) : color(this->color_inactive); const float percent = @@ -134,53 +134,52 @@ class SliderBase : public SliderOption, 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 +197,7 @@ class SliderBase : public SliderOption, public ComponentBase { if (captured_mouse_) { TakeFocus(); - return true; + return OnCapturedMouseEvent(event); } return false; @@ -242,19 +241,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_; diff --git a/src/ftxui/component/slider_test.cpp b/src/ftxui/component/slider_test.cpp index 21b96569..c8cd0652 100644 --- a/src/ftxui/component/slider_test.cpp +++ b/src/ftxui/component/slider_test.cpp @@ -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); diff --git a/src/ftxui/component/window.cpp b/src/ftxui/component/window.cpp index e33574b3..76907817 100644 --- a/src/ftxui/component/window.cpp +++ b/src/ftxui/component/window.cpp @@ -124,7 +124,7 @@ class WindowImpl : public ComponentBase, public WindowOptions { } private: - Element Render() final { + Element OnRender() final { auto element = ComponentBase::Render(); const bool captureable = diff --git a/src/ftxui/dom/border.cpp b/src/ftxui/dom/border.cpp index b428ac48..eb793b21 100644 --- a/src/ftxui/dom/border.cpp +++ b/src/ftxui/dom/border.cpp @@ -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 { diff --git a/src/ftxui/dom/box_helper.hpp b/src/ftxui/dom/box_helper.hpp index fbc85bd0..e7782dfa 100644 --- a/src/ftxui/dom/box_helper.hpp +++ b/src/ftxui/dom/box_helper.hpp @@ -5,6 +5,7 @@ #define FTXUI_DOM_BOX_HELPER_HPP #include +#include "ftxui/dom/requirement.hpp" namespace ftxui::box_helper { @@ -19,7 +20,6 @@ struct Element { }; void Compute(std::vector* elements, int target_size); - } // namespace ftxui::box_helper #endif /* end of include guard: FTXUI_DOM_BOX_HELPER_HPP */ diff --git a/src/ftxui/dom/dbox.cpp b/src/ftxui/dom/dbox.cpp index c6e752e7..8529114b 100644 --- a/src/ftxui/dom/dbox.cpp +++ b/src/ftxui/dom/dbox.cpp @@ -21,24 +21,21 @@ 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; - } } } diff --git a/src/ftxui/dom/flexbox.cpp b/src/ftxui/dom/flexbox.cpp index 336527b1..4396e2fe 100644 --- a/src/ftxui/dom/flexbox.cpp +++ b/src/ftxui/dom/flexbox.cpp @@ -89,6 +89,7 @@ class Flexbox : public Node { } void ComputeRequirement() override { + requirement_ = Requirement{}; for (auto& child : children_) { child->ComputeRequirement(); } @@ -103,12 +104,6 @@ class Flexbox : public Node { } 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()) { return; } @@ -130,19 +125,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); } } diff --git a/src/ftxui/dom/focus.cpp b/src/ftxui/dom/focus.cpp index b0d6cca4..887cd933 100644 --- a/src/ftxui/dom/focus.cpp +++ b/src/ftxui/dom/focus.cpp @@ -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 = false; + 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_; diff --git a/src/ftxui/dom/frame.cpp b/src/ftxui/dom/frame.cpp index 17619eed..43509147 100644 --- a/src/ftxui/dom/frame.cpp +++ b/src/ftxui/dom/frame.cpp @@ -6,28 +6,28 @@ #include // 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