1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 20:27:45 +00:00

LibGUI: Add gutter indicators to TextEditor :^)

HackStudio's Editor has displayed indicators in its gutter for a long
time, but each required manual code to paint them in the right place
and respond to click events. All indicators on a line would be painted
in the same location. If any other applications wanted to have gutter
indicators, they would also need to manually implement the same code.

This commit adds an API to GUI::TextEditor so it deals with these
indicators. It makes sure that multiple indicators on the same line
each have their own area to paint in, and provides a callback for when
one is clicked.

- `register_gutter_indicator()` should be called early on. It returns a
  `GutterIndicatorID` that is then used by the other methods.
  Indicators on a line are painted from right to left, in the order
  they were registered.
- `add_gutter_indicator()` and `remove_gutter_indicator()` add the
  indicator to the given line.
- `clear_gutter_indicators()` removes a given indicator from every line.
- The `on_gutter_click` callback is called whenever the user clicks on
  the gutter, but *not* on an indicator.
This commit is contained in:
Sam Atkins 2023-03-15 10:54:20 +00:00 committed by Andreas Kling
parent ce9b9a4f20
commit 4b29702fee
2 changed files with 118 additions and 1 deletions

View file

@ -290,6 +290,28 @@ void TextEditor::mousedown_event(MouseEvent& event)
}
}
if (gutter_content_rect(text_position.line()).contains(event.position())) {
auto& gutter_indicators = m_line_data[text_position.line()]->gutter_indicators;
auto indicator_position = 0;
for (auto i = 0u; i < m_gutter_indicators.size(); ++i) {
if ((gutter_indicators & (1 << i)) == 0)
continue;
if (gutter_indicator_rect(text_position.line(), indicator_position).contains(event.position())) {
auto& indicator_data = m_gutter_indicators[i];
if (indicator_data.on_click)
indicator_data.on_click(text_position.line(), event.modifiers());
return;
}
indicator_position++;
}
// We didn't click on an indicator
if (on_gutter_click)
on_gutter_click(text_position.line(), event.modifiers());
return;
}
if (on_mousedown)
on_mousedown();
@ -404,7 +426,7 @@ int TextEditor::gutter_width() const
{
if (!m_gutter_visible)
return 0;
return line_height(); // square gutter
return line_height() * (m_most_gutter_indicators_displayed_on_one_line + 1);
}
Gfx::IntRect TextEditor::gutter_content_rect(size_t line_index) const
@ -446,6 +468,18 @@ Gfx::IntRect TextEditor::folding_indicator_rect(size_t line_index) const
};
}
Gfx::IntRect TextEditor::gutter_indicator_rect(size_t line_number, int indicator_position) const
{
auto gutter_rect = gutter_content_rect(line_number);
auto indicator_size = gutter_rect.height();
return Gfx::IntRect {
gutter_rect.right() - static_cast<int>(lroundf(indicator_size * (indicator_position + 1.5f))),
gutter_rect.top(),
indicator_size,
indicator_size
};
}
Gfx::IntRect TextEditor::gutter_rect_in_inner_coordinates() const
{
return { 0, 0, gutter_width(), widget_inner_rect().height() };
@ -599,6 +633,28 @@ void TextEditor::paint_event(PaintEvent& event)
}
}
// Draw gutter indicators
if (m_gutter_visible) {
for (size_t line_index = first_visible_line; line_index <= last_visible_line; ++line_index) {
if (!document().line_is_visible(line_index))
continue;
auto& gutter_indicators = m_line_data[line_index]->gutter_indicators;
if (gutter_indicators == 0)
continue;
auto indicator_position = 0;
for (auto i = 0u; i < m_gutter_indicators.size(); ++i) {
if ((gutter_indicators & (1 << i)) == 0)
continue;
auto rect = gutter_indicator_rect(line_index, indicator_position);
m_gutter_indicators[i].draw_indicator(painter, rect, line_index);
indicator_position++;
}
}
}
auto horizontal_scrollbar_value = horizontal_scrollbar().value();
painter.translate(-horizontal_scrollbar_value, -vertical_scrollbar().value());
if (m_icon && horizontal_scrollbar_value > 0)
@ -2511,4 +2567,46 @@ void TextEditor::highlighter_did_set_folding_regions(Vector<GUI::TextDocumentFol
recompute_all_visual_lines();
}
ErrorOr<TextEditor::GutterIndicatorID> TextEditor::register_gutter_indicator(PaintGutterIndicator draw_indicator, OnGutterIndicatorClick on_click)
{
// We use a u32 to store a line's active gutter indicators, so that's the limit of how many we can have.
VERIFY(m_gutter_indicators.size() < 32);
GutterIndicatorID id = m_gutter_indicators.size();
TRY(m_gutter_indicators.try_empend(move(draw_indicator), move(on_click)));
return id;
}
void TextEditor::add_gutter_indicator(GutterIndicatorID id, size_t line)
{
auto& line_indicators = m_line_data[line]->gutter_indicators;
if (line_indicators & (1 << id.value()))
return;
line_indicators |= (1 << id.value());
// Ensure the gutter is at least wide enough to display all the indicators on this line.
if (m_most_gutter_indicators_displayed_on_one_line < m_gutter_indicators.size()) {
unsigned indicators_on_line = popcount(line_indicators);
if (indicators_on_line > m_most_gutter_indicators_displayed_on_one_line) {
m_most_gutter_indicators_displayed_on_one_line = indicators_on_line;
update();
}
}
update(gutter_content_rect(line));
}
void TextEditor::remove_gutter_indicator(GutterIndicatorID id, size_t line)
{
m_line_data[line]->gutter_indicators &= ~(1 << id.value());
update(gutter_content_rect(line));
}
void TextEditor::clear_gutter_indicators(GutterIndicatorID id)
{
for (auto line = 0u; line < line_count(); ++line)
remove_gutter_indicator(id, line);
update();
}
}

View file

@ -8,6 +8,7 @@
#pragma once
#include <AK/DistinctNumeric.h>
#include <AK/Function.h>
#include <LibCore/ElapsedTimer.h>
#include <LibCore/Timer.h>
@ -105,6 +106,15 @@ public:
bool is_gutter_visible() const { return m_gutter_visible; }
void set_gutter_visible(bool);
AK_TYPEDEF_DISTINCT_NUMERIC_GENERAL(size_t, GutterIndicatorID, CastToUnderlying, Comparison);
using PaintGutterIndicator = Function<void(Gfx::Painter&, Gfx::IntRect, size_t line)>;
using OnGutterIndicatorClick = Function<void(size_t line, unsigned modifiers)>;
ErrorOr<GutterIndicatorID> register_gutter_indicator(PaintGutterIndicator, OnGutterIndicatorClick = nullptr);
void add_gutter_indicator(GutterIndicatorID, size_t line);
void remove_gutter_indicator(GutterIndicatorID, size_t line);
void clear_gutter_indicators(GutterIndicatorID);
Gfx::IntRect gutter_indicator_rect(size_t line_number, int indicator_position) const;
void set_icon(Gfx::Bitmap const*);
Gfx::Bitmap const* icon() const { return m_icon; }
@ -113,6 +123,7 @@ public:
Function<void()> on_focusin;
Function<void()> on_focusout;
Function<void()> on_highlighter_change;
Function<void(size_t line, unsigned modifiers)> on_gutter_click;
void set_text(StringView, AllowCallback = AllowCallback::Yes);
void scroll_cursor_into_view();
@ -400,6 +411,13 @@ private:
int m_horizontal_content_padding { 3 };
TextRange m_selection {};
struct GutterIndicator {
PaintGutterIndicator draw_indicator;
OnGutterIndicatorClick on_click;
};
Vector<GutterIndicator> m_gutter_indicators;
unsigned m_most_gutter_indicators_displayed_on_one_line { 0 };
Optional<u32> m_substitution_code_point;
mutable OwnPtr<Vector<u32>> m_substitution_string_data; // Used to avoid repeated String construction.
@ -430,6 +448,7 @@ private:
struct LineData {
Vector<Utf32View> visual_lines;
Gfx::IntRect visual_rect;
u32 gutter_indicators { 0 }; // A bitfield of which gutter indicators are present. (1 << GutterIndicatorID)
};
Vector<NonnullOwnPtr<LineData>> m_line_data;