diff --git a/CHANGELOG.md b/CHANGELOG.md index fbe57ae25..75372602a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,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/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp index fa16080e2..efa83903c 100644 --- a/include/ftxui/dom/elements.hpp +++ b/include/ftxui/dom/elements.hpp @@ -121,8 +121,13 @@ Decorator selectionStyle(std::function style); // --- Layout is // Horizontal, Vertical or stacked set of elements. +// The extra `int` argument is used to select the `focus` element among the +// children. The `focus` element is the one that will be visible when the +// element is inside a `frame`. Element hbox(Elements); +Element hbox(Elements, int); Element vbox(Elements); +Element vbox(Elements, int); Element dbox(Elements); Element flexbox(Elements, FlexboxConfig config = FlexboxConfig()); Element gridbox(std::vector lines); @@ -161,7 +166,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/requirement.hpp b/include/ftxui/dom/requirement.hpp index 1b0a8842c..78906a822 100644 --- a/include/ftxui/dom/requirement.hpp +++ b/include/ftxui/dom/requirement.hpp @@ -5,6 +5,7 @@ #define FTXUI_DOM_REQUIREMENT_HPP #include "ftxui/screen/box.hpp" +#include "ftxui/screen/screen.hpp" namespace ftxui { @@ -20,13 +21,9 @@ struct Requirement { int flex_shrink_y = 0; // Focus management to support the frame/focus/select element. - enum Selection { - NORMAL = 0, - SELECTED = 1, - FOCUSED = 2, - }; - Selection selection = NORMAL; - Box selected_box; + bool is_focused = false; + Box focused_box; + Screen::Cursor::Shape cursor_shape = Screen::Cursor::Shape::Hidden; }; } // namespace ftxui diff --git a/src/ftxui/component/button.cpp b/src/ftxui/component/button.cpp index 844dd3966..35491cc15 100644 --- a/src/ftxui/component/button.cpp +++ b/src/ftxui/component/button.cpp @@ -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 b4b7f1583..9fdfc9264 100644 --- a/src/ftxui/component/checkbox.cpp +++ b/src/ftxui/component/checkbox.cpp @@ -26,13 +26,14 @@ class CheckboxBase : public ComponentBase, public CheckboxOption { Element Render() 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 { diff --git a/src/ftxui/component/container.cpp b/src/ftxui/component/container.cpp index 410bb9f5c..49c212d7f 100644 --- a/src/ftxui/component/container.cpp +++ b/src/ftxui/component/container.cpp @@ -107,7 +107,7 @@ class VerticalContainer : public ContainerBase { if (elements.empty()) { return text("Empty container") | reflect(box_); } - return vbox(std::move(elements)) | reflect(box_); + return vbox(std::move(elements), *selector_) | reflect(box_); } bool EventHandler(Event event) override { @@ -191,7 +191,7 @@ class HorizontalContainer : public ContainerBase { if (elements.empty()) { return text("Empty container"); } - return hbox(std::move(elements)); + return hbox(std::move(elements), *selector_); } bool EventHandler(Event event) override { @@ -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/input.cpp b/src/ftxui/component/input.cpp index 8f5d728c7..70b7c0872 100644 --- a/src/ftxui/component/input.cpp +++ b/src/ftxui/component/input.cpp @@ -98,7 +98,7 @@ class InputBase : public ComponentBase, public InputOption { // Component implementation: Element Render() override { const bool is_focused = Focused(); - const auto focused = (!is_focused && !hovered_) ? select + const auto focused = (!is_focused && !hovered_) ? focus : insert() ? focusCursorBarBlinking : focusCursorBlockBlinking; @@ -108,9 +108,6 @@ 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, diff --git a/src/ftxui/component/menu.cpp b/src/ftxui/component/menu.cpp index 223d3783d..7c68cd848 100644 --- a/src/ftxui/component/menu.cpp +++ b/src/ftxui/component/menu.cpp @@ -126,16 +126,13 @@ 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); + element |= focus; + element |= AnimatedColorStyle(i); + element |= reflect(boxes_[i]); + elements.push_back(element); } if (elements_postfix) { elements.push_back(elements_postfix()); @@ -145,28 +142,33 @@ 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_); } if (IsHorizontal()) { - return vbox({ - bar | xflex, - separatorHSelector(first_, second_, // - underline.color_active, - underline.color_inactive), - }) | + return vbox( + { + bar | xflex, + separatorHSelector(first_, second_, // + underline.color_active, + underline.color_inactive), + }, + 0) | reflect(box_); } else { - return hbox({ - separatorVSelector(first_, second_, // - underline.color_active, - underline.color_inactive), - bar | yflex, - }) | + return hbox( + { + separatorVSelector(first_, second_, // + underline.color_active, + underline.color_inactive), + bar | yflex, + }, + 0) | reflect(box_); } } @@ -630,8 +632,7 @@ Component MenuEntry(MenuEntryOption option) { (transform ? transform : DefaultOptionTransform) // (state); - auto focus_management = focused ? select : nothing; - return element | AnimatedColorStyle() | focus_management | reflect(box_); + return element | AnimatedColorStyle() | reflect(box_); } void UpdateAnimationTarget() { diff --git a/src/ftxui/component/radiobox.cpp b/src/ftxui/component/radiobox.cpp index 4c823ded0..4edcc2222 100644 --- a/src/ftxui/component/radiobox.cpp +++ b/src/ftxui/component/radiobox.cpp @@ -36,18 +36,15 @@ 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])); + elements.push_back(element | focus | reflect(boxes_[i])); } - return vbox(std::move(elements)) | reflect(box_); + return vbox(std::move(elements), focused_entry()) | reflect(box_); } // NOLINTNEXTLINE(readability-function-cognitive-complexity) diff --git a/src/ftxui/component/slider.cpp b/src/ftxui/component/slider.cpp index e8efbb857..1e4558676 100644 --- a/src/ftxui/component/slider.cpp +++ b/src/ftxui/component/slider.cpp @@ -134,53 +134,52 @@ class SliderBase : public SliderOption, public ComponentBase { return ComponentBase::OnEvent(event); } - bool OnMouseEvent(Event event) { - if (captured_mouse_) { - if (event.mouse().motion == Mouse::Released) { - captured_mouse_ = nullptr; - return true; - } + 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); - 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; - } + 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())); + 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; + if (old_value != this->value() && this->on_change) { + this->on_change(); + } + return true; + } + + bool OnMouseEvent(Event event) { + if (captured_mouse_) { + 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; @@ -243,7 +242,6 @@ class SliderWithLabel : public ComponentBase { } Element Render() override { - auto focus_management = Focused() ? focus : Active() ? select : nothing; auto gauge_color = (Focused() || mouse_hover_) ? color(Color::White) : color(Color::GrayDark); return hbox({ @@ -254,7 +252,7 @@ class SliderWithLabel : public ComponentBase { text("]"), }) | xflex, }) | - gauge_color | xflex | reflect(box_) | focus_management; + gauge_color | xflex | reflect(box_) | focus; } ConstStringRef label_; diff --git a/src/ftxui/dom/border.cpp b/src/ftxui/dom/border.cpp index b428ac48c..376384fe0 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,10 @@ 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.x_min++; + requirement_.focused_box.x_max++; + requirement_.focused_box.y_min++; + requirement_.focused_box.y_max++; } void SetBox(Box box) override { diff --git a/src/ftxui/dom/dbox.cpp b/src/ftxui/dom/dbox.cpp index c6e752e76..a54ceb2d0 100644 --- a/src/ftxui/dom/dbox.cpp +++ b/src/ftxui/dom/dbox.cpp @@ -27,7 +27,7 @@ class DBox : public Node { requirement_.flex_grow_y = 0; requirement_.flex_shrink_x = 0; requirement_.flex_shrink_y = 0; - requirement_.selection = Requirement::NORMAL; + requirement_.is_focused = false; for (auto& child : children_) { child->ComputeRequirement(); requirement_.min_x = @@ -35,9 +35,9 @@ class DBox : public Node { 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; + if (!requirement_.is_focused && child->requirement().is_focused) { + requirement_.is_focused = true; + requirement_.focused_box = child->requirement().focused_box; } } } diff --git a/src/ftxui/dom/flexbox.cpp b/src/ftxui/dom/flexbox.cpp index 336527b19..60c2c8688 100644 --- a/src/ftxui/dom/flexbox.cpp +++ b/src/ftxui/dom/flexbox.cpp @@ -104,8 +104,8 @@ class Flexbox : public Node { Layout(global, true); // Reset: - requirement_.selection = Requirement::Selection::NORMAL; - requirement_.selected_box = Box(); + requirement_.is_focused = false; + requirement_.focused_box = Box(); requirement_.min_x = 0; requirement_.min_y = 0; @@ -130,19 +130,19 @@ class Flexbox : public Node { // Find the selection: for (size_t i = 0; i < children_.size(); ++i) { - if (requirement_.selection >= children_[i]->requirement().selection) { + if (requirement_.is_focused || !children_[i]->requirement().is_focused) { continue; } - requirement_.selection = children_[i]->requirement().selection; - Box selected_box = children_[i]->requirement().selected_box; + requirement_.is_focused = true; + Box focused_box = children_[i]->requirement().focused_box; - // Shift |selected_box| according to its position inside this component: + // Shift |focused_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); + focused_box.x_min += b.x; + focused_box.y_min += b.y; + focused_box.x_max += b.x; + focused_box.y_max += b.y; + requirement_.focused_box = Box::Intersection(focused_box, box); } } diff --git a/src/ftxui/dom/focus.cpp b/src/ftxui/dom/focus.cpp index b0d6cca4d..5baaca747 100644 --- a/src/ftxui/dom/focus.cpp +++ b/src/ftxui/dom/focus.cpp @@ -36,9 +36,9 @@ Decorator focusPositionRelative(float x, float y) { void ComputeRequirement() override { NodeDecorator::ComputeRequirement(); - requirement_.selection = Requirement::Selection::NORMAL; + requirement_.is_focused = false; - Box& box = requirement_.selected_box; + Box& box = requirement_.focused_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_); @@ -75,9 +75,9 @@ Decorator focusPosition(int x, int y) { void ComputeRequirement() override { NodeDecorator::ComputeRequirement(); - requirement_.selection = Requirement::Selection::NORMAL; + requirement_.is_focused = 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 17619eed4..7e889cb76 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; + auto& focused_box = requirement_.focused_box; + focused_box.x_min = 0; + focused_box.y_min = 0; + focused_box.x_max = requirement_.min_x - 1; + focused_box.y_max = requirement_.min_y - 1; + requirement_.is_focused = true; } void SetBox(Box box) override { @@ -36,45 +36,6 @@ 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) @@ -87,14 +48,14 @@ class Frame : public Node { 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 +64,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 +91,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_.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