Skip to content

User data with multiple lines of text (new TextEdit widget) (#3130, #3131, #4743, #4968)#5455

Merged
dacap merged 27 commits into
betafrom
multiple-lines-textbox
Oct 14, 2025
Merged

User data with multiple lines of text (new TextEdit widget) (#3130, #3131, #4743, #4968)#5455
dacap merged 27 commits into
betafrom
multiple-lines-textbox

Conversation

@dacap

@dacap dacap commented Oct 6, 2025

Copy link
Copy Markdown
Member

This PR is a squashed and rebased version of #4968 to continue working in the multiple lines text editor.

@dacap dacap self-assigned this Oct 6, 2025
@aseprite-bot

Copy link
Copy Markdown
Collaborator

Hi there!

One or more of the commit messages in this PR do not match our code submission policy, please check the lint_commits CI job for more details on which commits were flagged and why.
Please do not close this PR and open another, instead modify your commit message(s) with git commit --amend and force push those changes to update this PR.

This was linked to issues Oct 6, 2025

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp Outdated
// Check the line width and if we're more than halfway past the line, we can set the caret to
// the full line.
caret.setPos(
(offsetPosition.x > m_lines[caret.line()].width / 2) ? m_lines[caret.line()].glyphCount : 0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: result of integer division used in a floating point context; possible loss of precision [bugprone-integer-division]

      (offsetPosition.x > m_lines[caret.line()].width / 2) ? m_lines[caret.line()].glyphCount : 0);
                          ^

Comment thread src/ui/textedit.cpp Outdated
}

for (int i = 0; i < run.glyphCount; ++i) {
gfx::RectF glyphBounds = run.getGlyphBounds(i).offset(gfx::PointF(0, lineStartY));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'glyphBounds' of type 'gfx::RectF' (aka 'RectT') can be declared 'const' [misc-const-correctness]

Suggested change
gfx::RectF glyphBounds = run.getGlyphBounds(i).offset(gfx::PointF(0, lineStartY));
gfx::RectF const glyphBounds = run.getGlyphBounds(i).offset(gfx::PointF(0, lineStartY));

Comment thread src/ui/textedit.cpp
s_timer->start();
}

void TextEdit::stopTimer()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: method 'stopTimer' can be made static [readability-convert-member-functions-to-static]

src/ui/textedit.h:160:

-   void stopTimer();
+   static void stopTimer();

Comment thread src/ui/textedit.cpp Outdated
Comment thread src/ui/textedit.cpp
Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:899: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:906: Frame #1: function 'leftWord' calls function 'left' here:

    if (!left())
         ^

src/ui/textedit.cpp:882: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:882: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp
return true;
}

bool TextEdit::Caret::leftWord()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'leftWord' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::leftWord()
                      ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:932: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:939: Frame #1: function 'rightWord' calls function 'right' here:

    if (!right())
         ^

src/ui/textedit.cpp:915: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:915: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

Comment thread src/ui/textedit.cpp
return true;
}

bool TextEdit::Caret::rightWord()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'rightWord' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::rightWord()
                      ^

Comment thread src/ui/textedit.h
void paste();
void selectAll();

obs::signal<void()> Change;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member variable 'Change' has public visibility [misc-non-private-member-variables-in-classes]

  obs::signal<void()> Change;
                      ^

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp
s_timer->start();
}

void TextEdit::stopTimer()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: method 'stopTimer' can be made static [readability-convert-member-functions-to-static]

src/ui/textedit.h:163:

-   void stopTimer();
+   static void stopTimer();

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:918: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:925: Frame #1: function 'leftWord' calls function 'left' here:

    if (!left())
         ^

src/ui/textedit.cpp:901: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:901: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:951: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:958: Frame #1: function 'rightWord' calls function 'right' here:

    if (!right())
         ^

src/ui/textedit.cpp:934: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:934: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:914: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:917: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:901: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:901: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:945: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:948: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:932: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:932: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:902: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:905: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:889: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:889: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:933: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:936: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:920: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:920: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp
requestFocus();

const auto* mouseMessage = static_cast<MouseMessage*>(msg);
Caret caret = caretFromPosition(mouseMessage->position());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'caret' of type 'Caret' can be declared 'const' [misc-const-correctness]

Suggested change
Caret caret = caretFromPosition(mouseMessage->position());
Caret const caret = caretFromPosition(mouseMessage->position());

Comment thread src/ui/textedit.cpp
s_timer->start();
}

void TextEdit::stopTimer()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: method 'stopTimer' can be made static [readability-convert-member-functions-to-static]

src/ui/textedit.h:178:

-   void stopTimer();
+   static void stopTimer();

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:916: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:919: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:903: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:903: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:947: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:950: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:934: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:934: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp
s_timer->start();
}

void TextEdit::stopTimer()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: method 'stopTimer' can be made static [readability-convert-member-functions-to-static]

src/ui/textedit.h:179:

-   void stopTimer();
+   static void stopTimer();

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:926: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:929: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:913: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:913: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:957: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:960: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:944: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:944: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/entry.cpp
void Entry::setCaretPos(const int pos)
{
gfx::Size caretSize = theme()->getEntryCaretSize(this);
gfx::Size caretSize = theme()->getCaretSize(this);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'caretSize' of type 'gfx::Size' (aka 'SizeT') can be declared 'const' [misc-const-correctness]

Suggested change
gfx::Size caretSize = theme()->getCaretSize(this);
gfx::Size const caretSize = theme()->getCaretSize(this);

Comment thread src/ui/textedit.cpp Outdated

// Ensure the caret height corresponds with the tallest glyph
m_caretRect.h = std::max(m_caretRect.h, line.height);
m_caretRect.y = point.y + line.height / 2 - m_caretRect.h / 2;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: result of integer division used in a floating point context; possible loss of precision [bugprone-integer-division]

      m_caretRect.y = point.y + line.height / 2 - m_caretRect.h / 2;
                                                  ^

Comment thread src/ui/textedit.cpp Outdated

// Ensure the caret height corresponds with the tallest glyph
m_caretRect.h = std::max(m_caretRect.h, line.height);
m_caretRect.y = point.y + line.height / 2 - m_caretRect.h / 2;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: result of integer division used in a floating point context; possible loss of precision [bugprone-integer-division]

      m_caretRect.y = point.y + line.height / 2 - m_caretRect.h / 2;
                                ^

Comment thread src/ui/theme.h
virtual void setDecorativeWidgetBounds(Widget* widget);
virtual int getScrollbarSize() { return kDefaultFontHeight; }
virtual gfx::Size getEntryCaretSize(Widget* widget) { return gfx::Size(kDefaultFontHeight, 1); }
virtual gfx::Size getCaretSize(Widget* widget) { return gfx::Size(kDefaultFontHeight, 1); }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: parameter 'widget' is unused [misc-unused-parameters]

Suggested change
virtual gfx::Size getCaretSize(Widget* widget) { return gfx::Size(kDefaultFontHeight, 1); }
virtual gfx::Size getCaretSize(Widget* /*widget*/) { return gfx::Size(kDefaultFontHeight, 1); }

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp Outdated

gfx::Rect TextEdit::caretBounds() const
{
auto& line = m_lines[m_caret.line()];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: 'auto &line' can be declared as 'const auto &line' [readability-qualified-auto]

Suggested change
auto& line = m_lines[m_caret.line()];
const auto& line = m_lines[m_caret.line()];

Comment thread src/ui/textedit.cpp
}

rc.h = std::max(rc.h, line.height);
rc.y = point.y + line.height / 2 - rc.h / 2;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: result of integer division used in a floating point context; possible loss of precision [bugprone-integer-division]

  rc.y = point.y + line.height / 2 - rc.h / 2;
                                     ^

Comment thread src/ui/textedit.cpp
}

rc.h = std::max(rc.h, line.height);
rc.y = point.y + line.height / 2 - rc.h / 2;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: result of integer division used in a floating point context; possible loss of precision [bugprone-integer-division]

  rc.y = point.y + line.height / 2 - rc.h / 2;
                   ^

Comment thread src/ui/textedit.cpp
return;

gfx::Point scroll = view->viewScroll();
gfx::Rect vp = view->viewportBounds();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'vp' of type 'gfx::Rect' (aka 'RectT') can be declared 'const' [misc-const-correctness]

Suggested change
gfx::Rect vp = view->viewportBounds();
gfx::Rect const vp = view->viewportBounds();

Comment thread src/ui/textedit.cpp
s_timer->start();
}

void TextEdit::stopTimer()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: method 'stopTimer' can be made static [readability-convert-member-functions-to-static]

src/ui/textedit.h:183:

-   void stopTimer();
+   static void stopTimer();

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:942: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:945: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:929: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:929: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:973: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:976: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:960: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:960: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp

gfx::Rect TextEdit::caretBounds() const
{
Line& line = m_caret.lineObj();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'line' of type 'Line &' can be declared 'const' [misc-const-correctness]

Suggested change
Line& line = m_caret.lineObj();
Line const& line = m_caret.lineObj();

Comment thread src/ui/textedit.cpp

// Check the line width and if we're more than halfway past the line, we can set the caret to
// the full line.
caret.setPos((offsetPosition.x > caret.lineObj().width / 2) ? caret.lineObj().glyphCount : 0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: result of integer division used in a floating point context; possible loss of precision [bugprone-integer-division]

    caret.setPos((offsetPosition.x > caret.lineObj().width / 2) ? caret.lineObj().glyphCount : 0);
                                     ^

Comment thread src/ui/textedit.cpp

// Keep the caret in a valid position.
if (!m_caret.isValid()) {
int line = std::clamp(m_caret.line(), 0, int(m_lines.size()) - 1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'line' of type 'int' can be declared 'const' [misc-const-correctness]

Suggested change
int line = std::clamp(m_caret.line(), 0, int(m_lines.size()) - 1);
int const line = std::clamp(m_caret.line(), 0, int(m_lines.size()) - 1);

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:945: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:948: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:932: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:932: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:976: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:979: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:963: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:963: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

@dacap dacap force-pushed the multiple-lines-textbox branch from 3a0f4ec to 66cb997 Compare October 8, 2025 20:46

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:935: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:938: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:922: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:922: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:966: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:969: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:953: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:953: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

Comment thread src/ui/theme.cpp
}
}

Theme::TextColors Theme::getTextColors(Widget* widget)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: parameter 'widget' is unused [misc-unused-parameters]

Suggested change
Theme::TextColors Theme::getTextColors(Widget* widget)
Theme::TextColors Theme::getTextColors(Widget* /*widget*/)

This fixes some asserts failing in TextEdit where the caret might be
outside the valid range just because we advance the caret one glyph
when in reality the inserted unicode string is a hidden char/variant.

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:937: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:940: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:924: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:924: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:968: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:971: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:955: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:955: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

Inserting an emoji with a variation at the end of a line was
introduced a lot of issues (saving the Widget text with the variation
in another place/line).
@dacap dacap assigned ckaiser and unassigned dacap Oct 9, 2025
@dacap

dacap commented Oct 9, 2025

Copy link
Copy Markdown
Member Author

@ckaiser would you mind to give a test to this? I think all missing parts are implemented and it's working pretty well (even inserting emojis/using IMEs).

The only missing part is for RTL languages, but that is something true for ui::Entry too.

@ckaiser

ckaiser commented Oct 12, 2025

Copy link
Copy Markdown
Member

Something I noticed, testing it on the user data input, fresh config, on macOS Tahoe:

It seems to not be excluded from the keyboard shortcut context, so I can't type capital letters since there's a bunch of Shift+[KEY] shortcuts, or even just the the C key (opens the canvas size selector)

This kinda makes it hard to test anything else since trying to type steals my focus and bounces me all over the place 😵

@ckaiser ckaiser assigned dacap and unassigned ckaiser Oct 12, 2025
@ckaiser

ckaiser commented Oct 13, 2025

Copy link
Copy Markdown
Member

Testing now on Windows:

  • Typing multiple lines and then selecting everything with Shift-A and pressing delete, leaves the caret in the second line, instead of at 0,0 as you'd expect, although I couldn't reproduce this consistently
  • The horizontal scrollbar isn't showing up when there's only one line (unless I create a new line with enter and then delete it)
  • Using shortcuts like ctrl-z undoes text but also undoes things on the canvas (same with pressing tab toggling the timeline)

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:955: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:958: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:942: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:942: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:986: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:989: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:973: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:973: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/textedit.cpp
{
Line& line = m_caret.lineObj();

gfx::Point origin = clientChildrenBounds().origin();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'origin' of type 'gfx::Point' (aka 'PointT') can be declared 'const' [misc-const-correctness]

Suggested change
gfx::Point origin = clientChildrenBounds().origin();
gfx::Point const origin = clientChildrenBounds().origin();

Comment thread src/ui/textedit.cpp Outdated
m_pos = pos;
}

bool TextEdit::Caret::left(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'left' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::left(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:941: example recursive call chain, starting from function 'leftWord'

bool TextEdit::Caret::leftWord()
                      ^

src/ui/textedit.cpp:944: Frame #1: function 'leftWord' calls function 'left' here:

  while (left()) {
         ^

src/ui/textedit.cpp:928: Frame #2: function 'left' calls function 'leftWord' here:

    return leftWord();
           ^

src/ui/textedit.cpp:928: ... which was the starting point of the recursive call chain; there may be other cycles

    return leftWord();
           ^

Comment thread src/ui/textedit.cpp Outdated
return true;
}

bool TextEdit::Caret::right(bool byWord)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'right' is within a recursive call chain [misc-no-recursion]

bool TextEdit::Caret::right(bool byWord)
                      ^
Additional context

src/ui/textedit.cpp:972: example recursive call chain, starting from function 'rightWord'

bool TextEdit::Caret::rightWord()
                      ^

src/ui/textedit.cpp:975: Frame #1: function 'rightWord' calls function 'right' here:

  while (right()) {
         ^

src/ui/textedit.cpp:959: Frame #2: function 'right' calls function 'rightWord' here:

    return rightWord();
           ^

src/ui/textedit.cpp:959: ... which was the starting point of the recursive call chain; there may be other cycles

    return rightWord();
           ^

@aseprite-bot aseprite-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread src/ui/entry.cpp Outdated
}
}
break;
Cmd cmd = cmdFromKeyMessage(keymsg);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'cmd' of type 'Cmd' can be declared 'const' [misc-const-correctness]

Suggested change
Cmd cmd = cmdFromKeyMessage(keymsg);
Cmd const cmd = cmdFromKeyMessage(keymsg);

Comment thread src/ui/entry.cpp
// combination.
else if (keymsg->scancode() >= kKeyFirstModifierScancode) {
return true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: do not use 'else' after 'return' [readability-else-after-return]

Suggested change
}
if (keymsg->scancode() >= kKeyFirstModifierScancode) {
return true;
}

Comment thread src/ui/textcmd.h

class TextCmdProcessor {
public:
enum Cmd {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: enum 'Cmd' uses a larger base type ('unsigned int', size: 4 bytes) than necessary for its value set, consider using 'std::uint8_t' (1 byte) as the base type to reduce its size [performance-enum-size]

  enum Cmd {
       ^

Comment thread src/ui/textedit.cpp Outdated
Comment thread src/ui/textedit.cpp
Comment thread src/ui/textedit.h Outdated
// TextCmdProcessor impl
bool onHasValidSelection() override { return !m_selection.isEmpty(); }
bool onCanModify() override { return true; }
void onExecuteCmd(Cmd cmd, base::codepoint_t unicodeChar, bool shiftPressed) override;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'ui::TextEdit::onExecuteCmd' has a definition with different parameter names [readability-inconsistent-declaration-parameter-name]

  void onExecuteCmd(Cmd cmd, base::codepoint_t unicodeChar, bool shiftPressed) override;
       ^
Additional context

src/ui/textedit.cpp:335: the definition seen here

void TextEdit::onExecuteCmd(const Cmd cmd,
               ^

src/ui/textedit.h:170: differing parameters are named here: ('shiftPressed'), in definition: ('expandSelection')

  void onExecuteCmd(Cmd cmd, base::codepoint_t unicodeChar, bool shiftPressed) override;
       ^

@dacap

dacap commented Oct 14, 2025

Copy link
Copy Markdown
Member Author

It seems to not be excluded from the keyboard shortcut context, so I can't type capital letters since there's a bunch of Shift+[KEY] shortcuts, or even just the the C key (opens the canvas size selector)

This kinda makes it hard to test anything else since trying to type steals my focus and bounces me all over the place 😵

Nice catch. There is something to avoid this for ui::Entry, just added the missing part for ui::TextEdit: 050126b

  • Typing multiple lines and then selecting everything with Shift-A and pressing delete, leaves the caret in the second line, instead of at 0,0 as you'd expect, although I couldn't reproduce this consistently

I wasn't able to reproduce it, but probably something to look in a future iteration.

  • The horizontal scrollbar isn't showing up when there's only one line (unless I create a new line with enter and then delete it)

Fixed 👍 (also fixed the whole scrolling/needed viewport size).

  • Using shortcuts like ctrl-z undoes text but also undoes things on the canvas (same with pressing tab toggling the timeline)

Known issue. It's a little hard to fix, as the undo is done for the whole sprite, the same happen in the (e.g.) Layer name field (ui::Entry fields). Something to fix in a future in some way 🤔

I'm merging this into beta 🚀

@dacap dacap force-pushed the multiple-lines-textbox branch from 2062d97 to 75ae1aa Compare October 14, 2025 21:56
@dacap dacap merged commit 8e7f892 into beta Oct 14, 2025
21 checks passed
@dacap dacap deleted the multiple-lines-textbox branch October 14, 2025 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Textbox UI logic User data with multiple lines

3 participants