[FarGroup/FarManager] master: Fullwidth-aware VMenu rendering and other improvements. (d30e72f05)

0 views
Skip to first unread message

farg...@farmanager.com

unread,
11:15 AM (6 hours ago) 11:15 AM
to farco...@googlegroups.com
Repository : https://github.com/FarGroup/FarManager
On branch : master
Link : https://github.com/FarGroup/FarManager/commit/d30e72f05318493b3fee627f6a2a2f43ab2d2b60

>---------------------------------------------------------------

commit d30e72f05318493b3fee627f6a2a2f43ab2d2b60
Author: Michael Z. Kadaner <MKad...@users.noreply.github.com>
Date: Sun Oct 19 20:15:55 2025 -0400

Fullwidth-aware VMenu rendering and other improvements.


>---------------------------------------------------------------

d30e72f05318493b3fee627f6a2a2f43ab2d2b60
far/changelog | 5 ++
far/common.tests.cpp | 81 +++++++++++++++---------------
far/common/algorithm.hpp | 16 ------
far/common/segment.hpp | 31 ++++++++++++
far/console.cpp | 5 +-
far/interf.cpp | 90 ++++++++++++++++++++++++++++++---
far/interf.hpp | 23 +++++++++
far/vbuild.m4 | 2 +-
far/vmenu.cpp | 128 +++++++++++++++++++++++++++--------------------
far/vmenu.hpp | 5 +-
10 files changed, 260 insertions(+), 126 deletions(-)

diff --git a/far/changelog b/far/changelog
index a2504b5a2..6374a6a10 100644
--- a/far/changelog
+++ b/far/changelog
@@ -1,3 +1,8 @@
+--------------------------------------------------------------------------------
+MZK 2026-02-28 10:54:01-05:00 - build 6651
+
+1. Fullwidth-aware VMenu rendering and other improvements.
+
--------------------------------------------------------------------------------
drkns 2026-02-28 00:39:34+00:00 - build 6650

diff --git a/far/common.tests.cpp b/far/common.tests.cpp
index 5d99af746..6690b5e76 100644
--- a/far/common.tests.cpp
+++ b/far/common.tests.cpp
@@ -330,46 +330,6 @@ TEST_CASE("algorithm.any_none_of")
STATIC_REQUIRE(none_of(1, 2, 3));
}

-TEST_CASE("algorithm.intersect.segments")
-{
- struct test_data
- {
- struct test_segment: public segment
- {
- test_segment(int const Begin, int const End)
- : segment{ Begin, segment::sentinel_tag{ End } }
- {}
- };
- test_segment A, B, Intersection;
- };
-
- static const test_data TestDataPoints[] =
- {
- { { 10, 20 }, { -1, 5 }, { 0, 0 } },
- { { 10, 20 }, { -1, 10 }, { 0, 0 } },
- { { 10, 20 }, { -1, 15 }, { 10, 15 } },
- { { 10, 20 }, { -1, 20 }, { 10, 20 } },
- { { 10, 20 }, { -1, 25 }, { 10, 20 } },
- { { 10, 20 }, { 10, 15 }, { 10, 15 } },
- { { 10, 20 }, { 10, 20 }, { 10, 20 } },
- { { 10, 20 }, { 10, 25 }, { 10, 20 } },
- { { 10, 20 }, { 15, 20 }, { 15, 20 } },
- { { 10, 20 }, { 15, 25 }, { 15, 20 } },
- { { 10, 20 }, { 20, 25 }, { 0, 0 } },
- { { 10, 20 }, { 25, 30 }, { 0, 0 } },
- { { 10, 20 }, { 0, 0 }, { 0, 0 } },
- { { 10, 20 }, { 15, 15 }, { 0, 0 } },
- { { 10, 20 }, { 20, 20 }, { 42, 42 } },
- { { 10, 20 }, { 30, 30 }, { 0, 0 } },
- };
-
- for (const auto& TestDataPoint : TestDataPoints)
- {
- REQUIRE(TestDataPoint.Intersection == intersect(TestDataPoint.A, TestDataPoint.B));
- REQUIRE(TestDataPoint.Intersection == intersect(TestDataPoint.B, TestDataPoint.A));
- }
-}
-
//----------------------------------------------------------------------------

#include "common/base64.hpp"
@@ -1328,6 +1288,47 @@ TEST_CASE("segment.iota")
}
}

+TEST_CASE("algorithm.intersect.segments")
+{
+ struct test_data
+ {
+ struct test_segment: public segment
+ {
+ test_segment(int const Begin, int const End)
+ : segment{ Begin, segment::sentinel_tag{ End } }
+ {
+ }
+ };
+ test_segment A, B, Intersection;
+ };
+
+ static const test_data TestDataPoints[] =
+ {
+ { { 10, 20 }, { -1, 5 }, { 0, 0 } },
+ { { 10, 20 }, { -1, 10 }, { 0, 0 } },
+ { { 10, 20 }, { -1, 15 }, { 10, 15 } },
+ { { 10, 20 }, { -1, 20 }, { 10, 20 } },
+ { { 10, 20 }, { -1, 25 }, { 10, 20 } },
+ { { 10, 20 }, { 10, 15 }, { 10, 15 } },
+ { { 10, 20 }, { 10, 20 }, { 10, 20 } },
+ { { 10, 20 }, { 10, 25 }, { 10, 20 } },
+ { { 10, 20 }, { 15, 20 }, { 15, 20 } },
+ { { 10, 20 }, { 15, 25 }, { 15, 20 } },
+ { { 10, 20 }, { 20, 25 }, { 0, 0 } },
+ { { 10, 20 }, { 25, 30 }, { 0, 0 } },
+ { { 10, 20 }, { 0, 0 }, { 0, 0 } },
+ { { 10, 20 }, { 15, 15 }, { 0, 0 } },
+ { { 10, 20 }, { 20, 20 }, { 42, 42 } },
+ { { 10, 20 }, { 30, 30 }, { 0, 0 } },
+ };
+
+ for (const auto& TestDataPoint : TestDataPoints)
+ {
+ REQUIRE(TestDataPoint.Intersection == intersect(TestDataPoint.A, TestDataPoint.B));
+ REQUIRE(TestDataPoint.Intersection == intersect(TestDataPoint.B, TestDataPoint.A));
+ }
+}
+
//----------------------------------------------------------------------------

#include "common/source_location.hpp"
diff --git a/far/common/algorithm.hpp b/far/common/algorithm.hpp
index 94acadda8..55a04e95e 100644
--- a/far/common/algorithm.hpp
+++ b/far/common/algorithm.hpp
@@ -34,7 +34,6 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "exception.hpp"
#include "preprocessor.hpp"
-#include "segment.hpp"
#include "type_traits.hpp"

#include <algorithm>
@@ -127,19 +126,4 @@ constexpr bool none_of(auto const&... Args)
return !any_of(Args...);
}

-template<typename TA, typename TB, typename TC = std::common_type_t<TA, TB>>
-segment_t<TC> intersect(segment_t<TA> const A, segment_t<TB> const B)
-{
- if (A.empty() || B.empty())
- return {};
-
- if (B.start() < A.start())
- return intersect(B, A);
-
- if (A.end() <= B.start())
- return {};
-
- return { static_cast<TC>(B.start()), typename segment_t<TC>::sentinel_tag{ std::min(static_cast<TC>(A.end()), static_cast<TC>(B.end())) } };
-}
-
#endif // ALGORITHM_HPP_BBD588C0_4752_46B2_AAB9_65450622FFF0
diff --git a/far/common/segment.hpp b/far/common/segment.hpp
index a1b17d301..dcfad4f32 100644
--- a/far/common/segment.hpp
+++ b/far/common/segment.hpp
@@ -36,6 +36,8 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <limits>
#include <ranges>

+#include "2d/rectangle.hpp"
+
//----------------------------------------------------------------------------

template<typename T>
@@ -92,6 +94,20 @@ public:
: segment_t{ InitialPoint, length_tag{ domain_max() } };
}

+ template<typename U>
+ [[nodiscard]]
+ static constexpr segment_t horizontal_extent(const rectangle_t<U>& rect) noexcept
+ {
+ return { rect.left, length_tag{ rect.width() } };
+ }
+
+ template<typename U>
+ [[nodiscard]]
+ static constexpr segment_t vertical_extent(const rectangle_t<U>& rect) noexcept
+ {
+ return { rect.top, length_tag{ rect.height() } };
+ }
+
private:
constexpr segment_t(T const Start, T const End) noexcept
: m_Start{ Start }
@@ -105,6 +121,21 @@ private:
T m_End{}; // One past last
};

+template<typename TA, typename TB, typename TC = std::common_type_t<TA, TB>>
+segment_t<TC> intersect(segment_t<TA> const A, segment_t<TB> const B)
+{
+ if (A.empty() || B.empty())
+ return {};
+
+ if (B.start() < A.start())
+ return intersect(B, A);
+
+ if (A.end() <= B.start())
+ return {};
+
+ return { static_cast<TC>(B.start()), typename segment_t<TC>::sentinel_tag{ std::min(static_cast<TC>(A.end()), static_cast<TC>(B.end())) } };
+}
+
using small_segment = segment_t<short>;
using segment = segment_t<int>;

diff --git a/far/console.cpp b/far/console.cpp
index 8afd70a2e..cb1558fb0 100644
--- a/far/console.cpp
+++ b/far/console.cpp
@@ -807,10 +807,9 @@ protected:
if (Size.x < MinimalSaneWidth || Size.y < MinimalSaneHeight)
{
LOGNOTICE(L"Console size [{}x{}] is too small, make it bigger"sv, Size.x, Size.y);
- Size.x = std::max(Size.x, 20);
- Size.y = std::max(Size.y, 10);
+ Size.x = std::max(Size.x, MinimalSaneWidth);
+ Size.y = std::max(Size.y, MinimalSaneHeight);
}
-
}

bool console::GetSize(point& Size, bool const Sanitize) const
diff --git a/far/interf.cpp b/far/interf.cpp
index 7d8d216cb..dbd53cd33 100644
--- a/far/interf.cpp
+++ b/far/interf.cpp
@@ -896,29 +896,103 @@ static void string_to_cells(string_view Str, size_t& CharsConsumed, cells& Cells
void chars_to_cells(string_view Str, size_t& CharsConsumed, size_t const CellsAvailable, size_t& CellsConsumed)
{
cells Cells;
- const auto& CellsToBeConsumed = Cells.emplace<0>();
+ const auto& CellsToBeConsumed = Cells.emplace<size_t>();
string_to_cells(Str, CharsConsumed, Cells, CellsAvailable);
CellsConsumed = CellsToBeConsumed;

-#ifdef _DEBUG
- if (CharsConsumed == Str.size())
- assert(CellsConsumed == visual_string_length(Str));
-#endif
+ assert(CharsConsumed < Str.size() || CellsConsumed == visual_string_length(Str));
}

-std::vector<FAR_CHAR_INFO> text_to_char_info(string_view Str, size_t CellsAvailable)
+std::vector<FAR_CHAR_INFO> text_to_char_info(string_view Str, size_t& CharsConsumed, size_t const CellsAvailable, size_t& CellsConsumed)
{
+ CharsConsumed = 0;
+ CellsConsumed = 0;
+
cells Cells;
- auto& Buffer = Cells.emplace<1>();
+ auto& Buffer = Cells.emplace<real_cells>();

if (Str.empty())
return Buffer;

- size_t CharsConsumed = 0;
string_to_cells(Str, CharsConsumed, Cells, CellsAvailable);
+ CellsConsumed = Buffer.size();
return Buffer;
}

+std::vector<FAR_CHAR_INFO> text_to_char_info(string_view Str, size_t CellsAvailable)
+{
+ size_t CharsConsumed{};
+ size_t CellsConsumed{};
+ return text_to_char_info(Str, CharsConsumed, CellsAvailable, CellsConsumed);
+}
+
+bool ClippedText(string_view Str, const segment ViewPort, bool& AllCharsConsumed)
+{
+ AllCharsConsumed = false;
+ if (ViewPort.empty()) return false;
+ if (CurX >= ViewPort.end()) return true;
+
+ const auto skip_text{ [](string_view Str, size_t& CharsConsumed, size_t const CellsAvailable, size_t& CellsConsumed)
+ {
+ chars_to_cells(Str, CharsConsumed, CellsAvailable, CellsConsumed);
+ } };
+
+ const auto write_text{ [](string_view Str, size_t& CharsConsumed, size_t const CellsAvailable, size_t& CellsConsumed)
+ {
+ Global->ScrBuf->Write(CurX, CurY, text_to_char_info(Str, CharsConsumed, CellsAvailable, CellsConsumed));
+ } };
+
+ // Returns true if a wide codepoint straddles across the EndX position.
+ const auto WriteOrSkip{ [&Str](const auto EndX, const auto Operation)
+ {
+ if (Str.empty() || CurX >= EndX) return false;
+
+ size_t CharsConsumed{};
+ size_t CellsConsumed{};
+ Operation(Str, CharsConsumed, EndX - CurX, CellsConsumed);
+ Str.remove_prefix(CharsConsumed);
+ CurX += static_cast<int>(CellsConsumed);
+
+ if (Str.empty() || CurX >= EndX) return false;
+
+ // Neither the string nor the screen area were exhausted.
+ // It must be a wide codepoint when only one cell is remaining.
+ assert(char_width::is_wide(encoding::utf16::extract_codepoint(Str)));
+ assert(CurX == EndX - 1);
+ return true;
+ } };
+
+ if (WriteOrSkip(ViewPort.start(), skip_text))
+ {
+ // A wide codepoint straddles the left edge of the clipping area.
+ // Prepare to continue with the visible part of the string.
+ // Skip the codepoint, advance CurX by two cells, and fill the trailing
+ // cell (where the right half of the glyph should have been) with a space.
+ encoding::utf16::remove_first_codepoint(Str);
+ CurX++;
+ Text(L' ');
+ }
+ if (WriteOrSkip(ViewPort.end(), write_text))
+ {
+ // A wide codepoint straddles the right edge of the clipping area.
+ // Fill the leading cell (where the left half of the glyph should have been)
+ // with a space and leave the codepoint in the input string to indicate
+ // that something was cut off by the right edge.
+ Text(L' ');
+ }
+
+ AllCharsConsumed = Str.empty();
+
+ assert(CurX <= ViewPort.end());
+ return CurX >= ViewPort.end();
+}
+
+bool ClippedText(string_view Str, const segment ViewPort)
+{
+ bool AllCharsConsumed{};
+ return ClippedText(Str, ViewPort, AllCharsConsumed);
+}
+
size_t Text(string_view Str, size_t const CellsAvailable)
{
auto Buffer = text_to_char_info(Str, CellsAvailable);
diff --git a/far/interf.hpp b/far/interf.hpp
index ee7976369..92fb290cf 100644
--- a/far/interf.hpp
+++ b/far/interf.hpp
@@ -44,6 +44,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "common/2d/matrix.hpp"
#include "common/2d/rectangle.hpp"
#include "common/function_ref.hpp"
+#include "common/segment.hpp"
#include "common/singleton.hpp"

// External:
@@ -179,6 +180,28 @@ std::vector<FAR_CHAR_INFO> text_to_char_info(string_view Str, size_t CellsAvaila

void Text(point Where, const FarColor& Color, string_view Str);

+// This function is handy when one needs to write a long string, only part of which
+// is visible within a viewport, and the string must be written by adjacent slices,
+// for example each slice in a different color. To accomplish this task, the caller
+// should set CurX somewhere to the left of ViewPort.end() and call this function
+// repeatedly for each slice passing the same ViewPort every time. When the function
+// returns true, the caller should stop calling it. More formally, the function:
+// - Lays out Str starting at CurX and writes the part visible within ViewPort.
+// - Returns true if CurX reached ViewPort.end(), i.e., the entire
+// ViewPort was written over, a.k.a. all cells were consumed.
+// - Advances CurX accordingly even if Str is entirely to the left of ViewPort.
+// - Never advances CurX beyond ViewPort.end() This is an optimization
+// to avoid scanning the part of the string clipped by ViewPort.end().
+// Supposedly after the function returned true, the caller should not care about CurX.
+// - If a wide character straddles ViewPort.start(), writes one padding whitespace
+// at the trailing cell of this character, i.e., at ViewPort.start().
+// - If a wide character straddles ViewPort.end(), writes one padding whitespace
+// at the leading cell of this character, i.e., at ViewPort.end() - 1.
+// In this case, the function returns true (reached the end of the ViewPort)
+// and sets AllCharsConsumed to false (something was cut off).
+bool ClippedText(string_view Str, const segment ViewPort, bool& AllCharsConsumed);
+bool ClippedText(string_view Str, const segment ViewPort);
+
size_t Text(string_view Str, size_t CellsAvailable);
size_t Text(string_view Str);

diff --git a/far/vbuild.m4 b/far/vbuild.m4
index 0ac58b3f4..d8b19232c 100644
--- a/far/vbuild.m4
+++ b/far/vbuild.m4
@@ -1 +1 @@
-6650
+6651
diff --git a/far/vmenu.cpp b/far/vmenu.cpp
index 55ca9f25b..15910e343 100644
--- a/far/vmenu.cpp
+++ b/far/vmenu.cpp
@@ -155,21 +155,25 @@ struct menu_layout
, ClientRect{ get_client_rect(Menu, BoxType) }
{
auto Left{ Menu.m_Where.left };
- if (need_box(BoxType)) LeftBox = Left++;
- if (need_check_mark()) CheckMark = Left++;
- if (const auto FixedColumnsWidth{ fixed_columns_width(Menu) })
+ if (Left <= Menu.m_Where.right && need_box(BoxType)) LeftBox = Left++;
+ if (Left < Menu.m_Where.right && need_check_mark()) CheckMark = Left++;
+ if (const auto FixedColumnsWidth{ fixed_columns_width(Menu) };
+ FixedColumnsWidth && Left + FixedColumnsWidth <= Menu.m_Where.right)
{
FixedColumnsArea = { Left, small_segment::length_tag{ FixedColumnsWidth } };
Left += FixedColumnsWidth;
}
- if (need_left_hscroll()) LeftHScroll = Left++;
+ if (Left < Menu.m_Where.right && need_left_hscroll()) LeftHScroll = Left++;

auto Right{ Menu.m_Where.right };
- if (need_box(BoxType)) RightBox = Right;
- if (need_scrollbar(Menu, BoxType)) Scrollbar = Right;
- if (RightBox || Scrollbar) Right--;
- if (need_submenu(Menu)) SubMenu = Right--;
- if (need_right_hscroll()) RightHScroll = Right--;
+ if (Right > Menu.m_Where.left)
+ {
+ if (need_box(BoxType)) RightBox = Right;
+ if (need_scrollbar(Menu, BoxType)) Scrollbar = Right;
+ if (RightBox || Scrollbar) Right--;
+ }
+ if (Right > Menu.m_Where.left && need_submenu(Menu)) SubMenu = Right--;
+ if (Right > Menu.m_Where.left && need_right_hscroll()) RightHScroll = Right--;

if (Left <= Right)
TextArea = { Left, small_segment::sentinel_tag{ static_cast<short>(Right + 1) } };
@@ -591,26 +595,37 @@ namespace
return std::clamp(NewHPos, HPosMin, HPosMax);
}

+ // |<------------ Text Area Width ------------>|
+ // | Item Text |
+ // |<---- NewHPos ----> | // NewHPos >= 0
+ // | Item Text |
+ // | <---- (1 - NewHPos) ---->| // NewHPos < 0
int get_item_smart_hpos(const int NewHPos, const int ItemLength, const int TextAreaWidth, const item_hscroll_policy Policy)
{
return get_item_absolute_hpos(NewHPos >= 0 ? NewHPos : TextAreaWidth - ItemLength + NewHPos + 1, ItemLength, TextAreaWidth, Policy);
}

+ // Left is m_HorizontalTracker->get_left_boundary()
+ // Right is m_HorizontalTracker->get_right_boundary()
int adjust_hpos_shift(const int Shift, const int Left, const int Right, const int TextAreaWidth)
{
- assert(Left < Right);
+ assert(Left <= Right);

- if (Shift == 0) return 0;
+ if (Shift == 0 || Left == Right) return 0;

- // Shift left.
+ // Shift item(s) right.
if (Shift > 0)
{
+ // The left boundary of the text block must stop at the right edge of the text area
const auto ShiftLimit{ std::max(TextAreaWidth - Left - 1, 0) };
+
+ // If the entire text block would be beyond the left edge of text area, pretend that it was exactly at edge
const auto GapLeftOfTextArea{ std::max(-Right, 0) };
+
return std::min(Shift + GapLeftOfTextArea, ShiftLimit);
}

- // Shift right. It's just shift left seen from behind the screen.
+ // Shift item(s) left. It's just shift right seen from behind the screen.
return -adjust_hpos_shift(-Shift, TextAreaWidth - Right, TextAreaWidth - Left, TextAreaWidth);
}

@@ -2353,7 +2368,12 @@ bool VMenu::SetItemHPos(menu_item_ex& Item, const auto& GetNewHPos)
if (Item.Flags & LIF_SEPARATOR) return false;

const auto ItemLength{ GetItemVisualLength(Item) };
- if (ItemLength <= 0) return false;
+ if (ItemLength <= 0)
+ {
+ assert(Item.HorizontalPosition == 0);
+ m_HorizontalTracker->update_item_hpos(Item.HorizontalPosition, 0, 0, 0);
+ return false;
+ }

const auto NewHPos = [&]
{
@@ -2457,7 +2477,10 @@ bool VMenu::AlignAnnotations()

const auto Guard{ m_HorizontalTracker->start_bulk_update_annotation(AlignPos) };
return SetAllItemsHPos(
- [&](const menu_item_ex& Item) { return AlignPos - Item.SafeGetFirstAnnotation(); });
+ [&](const menu_item_ex& Item)
+ {
+ return AlignPos - static_cast<int>(visual_string_length(GetItemText(Item).substr(0, Item.SafeGetFirstAnnotation())));
+ });
}

bool VMenu::ToggleFixedColumns()
@@ -2746,11 +2769,11 @@ void VMenu::DrawTitles() const
set_color(Colors, color_indices::Title);

GotoXY(m_Where.left + 2, m_Where.bottom);
- MenuText(m_HorizontalTracker->get_debug_string());
+ ClippedText(m_HorizontalTracker->get_debug_string(), segment::horizontal_extent(m_Where));

const auto TextAreaWidthLabel{ far::format(L" [{}] "sv, CalculateTextAreaWidth()) };
- GotoXY(m_Where.right - 1 - static_cast<int>(TextAreaWidthLabel.size()), m_Where.bottom);
- MenuText(TextAreaWidthLabel);
+ GotoXY(m_Where.right - 1 - static_cast<int>(visual_string_length(TextAreaWidthLabel)), m_Where.bottom);
+ ClippedText(TextAreaWidthLabel, segment::horizontal_extent(m_Where));
}
}

@@ -2810,7 +2833,7 @@ void VMenu::DrawSeparator(const size_t ItemIndex, const int BoxType, const int Y
ApplySeparatorName(Items[ItemIndex], separator);
set_color(Colors, color_indices::Separator);
GotoXY(m_Where.left, Y);
- MenuText(separator);
+ ClippedText(separator, segment::horizontal_extent(m_Where));
}

void VMenu::ConnectSeparator(const size_t ItemIndex, string& separator, const int BoxType) const
@@ -2911,15 +2934,11 @@ void VMenu::DrawFixedColumns(
const segment CellArea{ CurCellAreaStart, segment::length_tag{ CurFixedColumn.CurrentWidth } };

const auto CellText{ get_item_cell_text(Item.Name, CurFixedColumn.TextSegment) };
- const auto VisibleCellTextSegment{ intersect(
- segment{ 0, segment::length_tag{ static_cast<segment::domain_t>(CellText.size()) } },
- segment{ 0, segment::length_tag{ CellArea.length()}})};
-
- if (!VisibleCellTextSegment.empty())
- MenuText(CellText.substr(VisibleCellTextSegment.start(), VisibleCellTextSegment.length()));
+ if (!ClippedText(CellText, CellArea))
+ ClippedText(BlankLine, CellArea);

- MenuText(BlankLine.substr(0, CellArea.end() - WhereX()));
- MenuText(CurFixedColumn.Separator);
+ assert(WhereX() < FixedColumnsArea.end());
+ Text(CurFixedColumn.Separator);

CurCellAreaStart = CellArea.end() + 1;
}
@@ -2935,10 +2954,16 @@ bool VMenu::DrawItemText(
std::vector<int>& HighlightMarkup,
string_view BlankLine) const
{
- GotoXY(TextArea.start(), Y);
- set_color(Colors, ColorIndices.Normal);
+ const segment Bounds{ TextArea.start(), segment::sentinel_tag{ TextArea.end() } };

- Text(BlankLine.substr(0, std::clamp(Item.HorizontalPosition, 0, static_cast<int>(TextArea.length()))));
+ if (const auto Indent{ std::max(Item.HorizontalPosition, 0) }; Indent > 0)
+ {
+ GotoXY(Bounds.start(), Y);
+ set_color(Colors, ColorIndices.Normal);
+ bool AllCharsConsumed{};
+ if (ClippedText(BlankLine.substr(0, Indent), Bounds, AllCharsConsumed)) return true;
+ assert(WhereX() == Bounds.start() + Indent);
+ }

const auto [ItemText, HighlightPos]{ [&]{
const auto RawItemText_{ GetItemText(Item) };
@@ -2953,36 +2978,42 @@ bool VMenu::DrawItemText(
return std::tuple{ ItemText_, HighlightPos_ };
}() };

- const auto VisibleTextSegment{ intersect(
- segment{ 0, segment::length_tag{ static_cast<segment::domain_t>(ItemText.size()) } },
- segment::ray(-Item.HorizontalPosition))};
+ bool NeedRightHScroll{};

- if (!VisibleTextSegment.empty())
+ if (!ItemText.empty())
{
- markup_slice_boundaries(VisibleTextSegment, Item.Annotations, HighlightPos, HighlightMarkup);
+ const segment TextSegment{ 0, segment::length_tag{ static_cast<segment::domain_t>(ItemText.size()) } };
+ markup_slice_boundaries(TextSegment, Item.Annotations, HighlightPos, HighlightMarkup);
+
+ GotoXY(Bounds.start() + Item.HorizontalPosition, Y);

auto CurColorIndex{ ColorIndices.Normal };
auto AltColorIndex{ ColorIndices.Highlighted };
- auto CurTextPos{ VisibleTextSegment.start() };
+ int CurTextPos{};

for (const auto SliceEnd : HighlightMarkup)
{
set_color(Colors, CurColorIndex);
- Text(string_view{ ItemText }.substr(CurTextPos, SliceEnd - CurTextPos), TextArea.end() - WhereX());
+ bool AllCharsConsumed{};
+ if (ClippedText(string_view{ ItemText }.substr(CurTextPos, SliceEnd - CurTextPos), Bounds, AllCharsConsumed))
+ {
+ NeedRightHScroll = !AllCharsConsumed || SliceEnd < static_cast<int>(ItemText.size());
+ break;
+ }
std::ranges::swap(CurColorIndex, AltColorIndex);
CurTextPos = SliceEnd;
}
}

- set_color(Colors, ColorIndices.Normal);
-
- if (WhereX() < TextArea.end())
+ if (WhereX() < Bounds.end())
{
- Text(BlankLine, TextArea.end() - WhereX());
- assert(WhereX() == TextArea.end());
+ GotoXY(std::max(WhereX(), Bounds.start()), Y);
+ set_color(Colors, ColorIndices.Normal);
+ ClippedText(BlankLine, Bounds);
+ assert(WhereX() == Bounds.end());
}

- return Item.HorizontalPosition + static_cast<int>(visual_string_length(ItemText)) > TextArea.length();
+ return NeedRightHScroll;
}

int VMenu::CheckHighlights(wchar_t CheckSymbol, int StartPos) const
@@ -3539,7 +3570,6 @@ const UUID& VMenu::Id() const
return MenuId;
}

-// Consider: Do we need this function? Maybe client should rely on VMENU_AUTOHIGHLIGHT?
std::vector<string> VMenu::AddHotkeys(std::span<menu_item> const MenuItems)
{
std::vector<string> Result(MenuItems.size());
@@ -3599,16 +3629,6 @@ string_view VMenu::GetItemText(const menu_item_ex& Item) const
return get_item_cell_text(Item.Name, m_ItemTextSegment);
}

-size_t VMenu::MenuText(string_view const Str) const
-{
- return Text(Str, m_Where.width() - (WhereX() - m_Where.left));
-}
-
-size_t VMenu::MenuText(wchar_t const Char) const
-{
- return MenuText({ &Char, 1 });
-}
-
#ifdef ENABLE_TESTS

#include "testing.hpp"
diff --git a/far/vmenu.hpp b/far/vmenu.hpp
index d0f5dbe23..52e056ba4 100644
--- a/far/vmenu.hpp
+++ b/far/vmenu.hpp
@@ -145,7 +145,7 @@ struct menu_item_ex: menu_item
wchar_t AutoHotkey{};
size_t AutoHotkeyPos{};

- int SafeGetFirstAnnotation() const noexcept { return Annotations.empty() ? 0 : Annotations.front().start(); }
+ int SafeGetFirstAnnotation() const noexcept { return Annotations.empty() || Annotations.front().empty() ? 0 : Annotations.front().start(); }
};

struct item_color_indices;
@@ -348,9 +348,6 @@ private:

int sizeAsInt() const { return static_cast<int>(size()); }

- size_t MenuText(string_view Str) const;
- size_t MenuText(wchar_t Char) const;
-
string strTitle;
string strBottomTitle;
int SelectPos{-1};


Reply all
Reply to author
Forward
0 new messages