Started work on TextEdit widget

This commit is contained in:
OmniaX-Dev 2026-05-07 06:39:43 +02:00
parent 655d7b1f98
commit a3eb4ba3cc
17 changed files with 900 additions and 351 deletions

17
build
View file

@ -43,5 +43,22 @@ else
cd bin cd bin
./ostd_test ./ostd_test
cd .. cd ..
elif [[ "$1" == "dbg" ]]; then
printf "\n\033[0;32mDebugging Test Application...\n\n\033[0m"
cd bin
gdb -batch \
-ex 'set pagination off' \
-ex 'set print pretty on' \
-ex 'set logging file ../gdb-last.log' \
-ex 'set logging overwrite on' \
-ex 'set logging on' \
-ex 'handle SIGPIPE nostop noprint pass' \
-ex run \
-ex 'bt full' \
-ex 'info locals' \
-ex 'info args' \
-ex quit \
--args ./ostd_test
cd ..
fi fi
fi fi

View file

@ -388,3 +388,14 @@ macro set_common_values(_borderColor = $borderColor, _fontSize = 18, _showBorder
set_focus_border($blueAccent) set_focus_border($blueAccent)
} }
% ====================== % ======================
% ====== TextEdit ======
(textEdit) {
set_common_values()
set_focus_border($blueAccent)
backgroundColor = $darkColor
textColor = $textColor
}
% ======================

View file

@ -29,6 +29,9 @@
***FIX: KeyPress crash on TreeView ***FIX: KeyPress crash on TreeView
***FIX: Animated icons not working in TreeView ***FIX: Animated icons not working in TreeView
***Check if possible, then change all WindowCore& refs to Window& ***Check if possible, then change all WindowCore& refs to Window&
***Add utf-8 codepoint walking functions to String
***Change the stored text in KeyEventData from a single char to the full utf-8 string
***Check if Text Entered events are enabled via SDL
Implement cursors in Stylesheet Implement cursors in Stylesheet
FIX: Window getting exponentially bigger when Desktop scale changes FIX: Window getting exponentially bigger when Desktop scale changes
FIX: Refreshing scroll when content extent changes FIX: Refreshing scroll when content extent changes
@ -56,17 +59,17 @@ FIX: ContextMenu rendering partly outside the screen when showing it close to th
Add ToolBar::addButton overload that takes a ogfx::Icon wrapper Add ToolBar::addButton overload that takes a ogfx::Icon wrapper
Redesign theme overrides to be more robust Redesign theme overrides to be more robust
Add modifiers to Event.keyboard path Add modifiers to Event.keyboard path
Implement a font-fallback chain in FontGlyphAtlas
Implement appendText in TextBuffer and TextEdit
Implement per-glyph caching in BasicRenderer2D
Added double-click event to widgets
Implement shift-selection in TextEdit (mouse/keyboard)
Implement ctrl-selection/navigation in TextEdit
Implement home/end and pg up/pg down in TextEdit
Text:
Check if Text Entered events are enabled via SDL
Add utf-8 codepoint walking functions to String
Change the stored text in KeyEventData from a single char to the full utf-8 string
Implement a font-fallback chain in FontGlyphAtlas
Implement following widgets: Implement following widgets:

View file

@ -1 +1 @@
2076 2077

View file

@ -70,7 +70,7 @@ namespace ogfx
{ {
public: enum class eKeyEvent { Pressed = 0, Released, Text }; public: enum class eKeyEvent { Pressed = 0, Released, Text };
public: public:
inline KeyEventData(WindowCore& parent, i32 key, char _text, eKeyEvent evt) : parentWindow(parent), keyCode(key), text(_text), eventType(evt) inline KeyEventData(WindowCore& parent, i32 key, const String& _text, eKeyEvent evt) : parentWindow(parent), keyCode(key), text(_text), eventType(evt)
{ {
setTypeName("ogfx::KeyEventData"); setTypeName("ogfx::KeyEventData");
validate(); validate();
@ -78,7 +78,7 @@ namespace ogfx
public: public:
i32 keyCode; i32 keyCode;
char text; String text;
eKeyEvent eventType; eKeyEvent eventType;
WindowCore& parentWindow; WindowCore& parentWindow;
}; };

View file

@ -1,27 +1,27 @@
/* /*
OmniaFramework - A collection of useful functionality OmniaFramework - A collection of useful functionality
Copyright (C) 2026 OmniaX-Dev Copyright (C) 2026 OmniaX-Dev
This file is part of OmniaFramework. This file is part of OmniaFramework.
OmniaFramework is free software: you can redistribute it and/or modify OmniaFramework is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
OmniaFramework is distributed in the hope that it will be useful, OmniaFramework is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>. along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include "RawTextInput.hpp" #include "RawTextInput.hpp"
#include "../gui/Window.hpp" #include "../gui/Window.hpp"
#include "../../io/Logger.hpp" #include "../../io/Logger.hpp"
#include "utils/Keycodes.hpp" #include "../utils/Keycodes.hpp"
namespace ogfx namespace ogfx
{ {
@ -55,9 +55,9 @@ namespace ogfx
OX_ERROR("Invalid character filter in RawTextInput event listener."); OX_ERROR("Invalid character filter in RawTextInput event listener.");
return; //TODO: Better error return; //TODO: Better error
} }
if (parent.m_charFilter->isValidChar(data.text)) if (parent.m_charFilter->isValidChar(data.text[0]))
{ {
if (parent.m_lastChar == data.text && parent.m_keyRepeatCounter.isCounting()) if (parent.m_lastChar == data.text[0] && parent.m_keyRepeatCounter.isCounting())
{ {
onSignalHandled(signal); onSignalHandled(signal);
return; return;
@ -66,10 +66,10 @@ namespace ogfx
String s2 = ""; String s2 = "";
if (parent.m_cursorPosition < parent.m_text.len()) if (parent.m_cursorPosition < parent.m_text.len())
s2 = parent.m_text.new_substr(parent.m_cursorPosition); s2 = parent.m_text.new_substr(parent.m_cursorPosition);
s1.addChar(data.text).add(s2); s1.addChar(data.text[0]).add(s2);
parent.m_text = s1; parent.m_text = s1;
parent.m_cursorPosition++; parent.m_cursorPosition++;
parent.m_lastChar = data.text; parent.m_lastChar = data.text[0];
parent.m_keyRepeatCounter.start(); parent.m_keyRepeatCounter.start();
if (parent.m_theme.cursorBlink) if (parent.m_theme.cursorBlink)
parent.m_theme.cursorBlinkCounter.start(); parent.m_theme.cursorBlinkCounter.start();

View file

@ -217,28 +217,6 @@ namespace ogfx
} }
} }
void WidgetManager::onKeyPressed(const Event& event)
{
auto focused = m_window.getFocusManager().getFocused();
if (!focused || !focused->isVisible()) return;
focused->__onKeyPressed(event);
}
void WidgetManager::onKeyReleased(const Event& event)
{
auto focus = m_window.getFocusManager();
auto focused = focus.getFocused();
if (!focused || !focused->isVisible()) return;
focused->__onKeyReleased(event);
}
void WidgetManager::onTextEntered(const Event& event)
{
auto focused = m_window.getFocusManager().getFocused();
if (!focused || !focused->isVisible()) return;
focused->__onTextEntered(event);
}
void WidgetManager::onWindowClosed(const Event& event) void WidgetManager::onWindowClosed(const Event& event)
{ {
for (auto& w : m_widgetList) for (auto& w : m_widgetList)

View file

@ -50,9 +50,6 @@ namespace ogfx
void onMouseMoved(const Event& event); void onMouseMoved(const Event& event);
void onMouseScrolled(const Event& event); void onMouseScrolled(const Event& event);
void onMouseDragged(const Event& event); void onMouseDragged(const Event& event);
void onKeyPressed(const Event& event);
void onKeyReleased(const Event& event);
void onTextEntered(const Event& event);
void onWindowClosed(const Event& event); void onWindowClosed(const Event& event);
void onWindowResized(const Event& event); void onWindowResized(const Event& event);
void onWindowFocused(const Event& event); void onWindowFocused(const Event& event);

View file

@ -30,6 +30,7 @@
#include <ogfx/gui/widgets/Slider.hpp> #include <ogfx/gui/widgets/Slider.hpp>
#include <ogfx/gui/widgets/ComboBox.hpp> #include <ogfx/gui/widgets/ComboBox.hpp>
#include <ogfx/gui/widgets/TreeView.hpp> #include <ogfx/gui/widgets/TreeView.hpp>
#include <ogfx/gui/widgets/Text.hpp>
namespace ogfx namespace ogfx
{ {

View file

@ -617,21 +617,21 @@ namespace ogfx
MouseEventData mmd = get_mouse_state(event); MouseEventData mmd = get_mouse_state(event);
ostd::SignalHandler::emitSignal(ostd::BuiltinSignals::MouseReleased, ostd::Signal::Priority::RealTime, mmd); ostd::SignalHandler::emitSignal(ostd::BuiltinSignals::MouseReleased, ostd::Signal::Priority::RealTime, mmd);
} }
else if (event.type == SDL_EVENT_TEXT_INPUT)
{
KeyEventData ked(*this, 0, event.text.text, KeyEventData::eKeyEvent::Text);
ostd::SignalHandler::emitSignal(ostd::BuiltinSignals::TextEntered, ostd::Signal::Priority::RealTime, ked);
}
else if (event.type == SDL_EVENT_KEY_DOWN) else if (event.type == SDL_EVENT_KEY_DOWN)
{ {
KeyEventData ked(*this, (i32)event.key.key, 0, KeyEventData::eKeyEvent::Pressed); KeyEventData ked(*this, (i32)event.key.key, "", KeyEventData::eKeyEvent::Pressed);
ostd::SignalHandler::emitSignal(ostd::BuiltinSignals::KeyPressed, ostd::Signal::Priority::RealTime, ked); ostd::SignalHandler::emitSignal(ostd::BuiltinSignals::KeyPressed, ostd::Signal::Priority::RealTime, ked);
} }
else if (event.type == SDL_EVENT_KEY_UP) else if (event.type == SDL_EVENT_KEY_UP)
{ {
KeyEventData ked(*this, (i32)event.key.key, 0, KeyEventData::eKeyEvent::Released); KeyEventData ked(*this, (i32)event.key.key, "", KeyEventData::eKeyEvent::Released);
ostd::SignalHandler::emitSignal(ostd::BuiltinSignals::KeyReleased, ostd::Signal::Priority::RealTime, ked); ostd::SignalHandler::emitSignal(ostd::BuiltinSignals::KeyReleased, ostd::Signal::Priority::RealTime, ked);
} }
else if (event.type == SDL_EVENT_TEXT_INPUT)
{
KeyEventData ked(*this, 0, event.text.text[0], KeyEventData::eKeyEvent::Text);
ostd::SignalHandler::emitSignal(ostd::BuiltinSignals::TextEntered, ostd::Signal::Priority::RealTime, ked);
}
__on_event(event); __on_event(event);
} }
@ -1002,7 +1002,9 @@ namespace ogfx
evt.__original_signal_id = ostd::BuiltinSignals::KeyPressed; evt.__original_signal_id = ostd::BuiltinSignals::KeyPressed;
m_toolbar.__onKeyPressed(evt); m_toolbar.__onKeyPressed(evt);
m_statusbar.__onKeyPressed(evt); m_statusbar.__onKeyPressed(evt);
m_rootWidget.__onKeyPressed(evt); auto focused = m_focusManager.getFocused();
if (focused)
focused->__onKeyPressed(evt);
} }
else if (signal.ID == ostd::BuiltinSignals::KeyReleased) else if (signal.ID == ostd::BuiltinSignals::KeyReleased)
{ {
@ -1011,7 +1013,9 @@ namespace ogfx
m_focusManager.onKeyReleased(evt); m_focusManager.onKeyReleased(evt);
m_toolbar.__onKeyReleased(evt); m_toolbar.__onKeyReleased(evt);
m_statusbar.__onKeyReleased(evt); m_statusbar.__onKeyReleased(evt);
m_rootWidget.__onKeyReleased(evt); auto focused = m_focusManager.getFocused();
if (focused)
focused->__onKeyReleased(evt);
if (evt.keyboard->keyCode == KeyCode::Escape) if (evt.keyboard->keyCode == KeyCode::Escape)
close(); close();
} }
@ -1021,7 +1025,9 @@ namespace ogfx
evt.__original_signal_id = ostd::BuiltinSignals::TextEntered; evt.__original_signal_id = ostd::BuiltinSignals::TextEntered;
m_toolbar.__onTextEntered(evt); m_toolbar.__onTextEntered(evt);
m_statusbar.__onTextEntered(evt); m_statusbar.__onTextEntered(evt);
m_rootWidget.__onTextEntered(evt); auto focused = m_focusManager.getFocused();
if (focused)
focused->__onTextEntered(evt);
} }
onSignal(signal); onSignal(signal);
} }

View file

@ -19,11 +19,427 @@
*/ */
#include "Text.hpp" #include "Text.hpp"
#include "../../utils/Keycodes.hpp"
#include "../../render/BasicRenderer.hpp"
namespace ostd namespace ogfx
{ {
namespace gui namespace gui
{ {
TextEdit& TextEdit::create(void)
{
setPadding({ 0, 0, 0, 0 });
setTypeName("ogfx::gui::TextEdit");
disableChildren();
setStylesheetCategoryName("textEdit");
enableFocus();
enableBackground();
enableBorder();
enableTextEnteredEvent(true);
setSize({ 200, 30 });
m_cursorBlinkTimer.start();
m_buffer.setOnChanged([this](const ostd::TextBuffer&) {
m_layout.dirty = true;
});
m_buffer.setOnCursorMoved([this](const ostd::TextBuffer&) {
if (m_cursorBlink)
m_cursorBlinkTimer.start();
m_cursorState = true;
});
validate();
return *this;
}
void TextEdit::applyTheme(const ostd::Stylesheet& theme)
{
}
void TextEdit::onDraw(ogfx::BasicRenderer2D& gfx)
{
const auto bounds = getGlobalBounds();
// -------------------------------------------------------------
// 1. Rebuild the layout cache if the buffer (or font/size) has
// changed since last frame. We mark dirty in the buffer's
// onChanged callback (set up in create()).
// -------------------------------------------------------------
if (m_layout.dirty)
rebuild_layout(gfx);
// -------------------------------------------------------------
// 2. Make sure the cursor is in view. This adjusts m_scrollX as
// needed so the cursor sits inside the widget's content area.
// -------------------------------------------------------------
ensure_cursor_visible(bounds);
// -------------------------------------------------------------
// 3. Compute the text content area inside the widget — the
// rectangle where text is allowed to render. Anything drawn
// outside this rect gets clipped away.
// -------------------------------------------------------------
const Rectangle contentArea {
bounds.x + m_textPadding.left,
bounds.y,
bounds.w - m_textPadding.left - m_textPadding.right,
bounds.h
};
// -------------------------------------------------------------
// 5. Compute the y position for the text baseline area. Vertically
// centered using the cached line height. This is single-line
// behavior; for multiline you'd loop over lines and offset y
// by lineHeight per row.
// -------------------------------------------------------------
const f32 textY = bounds.y + (bounds.h - m_layout.lineHeight) * 0.5f;
const f32 textX = contentArea.x - m_scrollX;
// -------------------------------------------------------------
// 6. Draw the selection highlight FIRST (under the text). This
// way the text stays the regular color and just sits on top
// of a colored band — like every text editor.
// -------------------------------------------------------------
if (m_buffer.hasSelection())
{
const u32 selStart = m_buffer.selectionStart();
const u32 selEnd = m_buffer.selectionEnd();
const f32 x1 = textX + layout_x_for_byte(selStart);
const f32 x2 = textX + layout_x_for_byte(selEnd);
gfx.fillRect(
{ x1, textY, x2 - x1, m_layout.lineHeight },
m_selectionColor
);
}
// -------------------------------------------------------------
// 7. Draw the text itself. Single drawString call — the layout
// cache is only used for cursor / selection positioning;
// actual glyph rendering goes through the renderer's existing
// code path (which handles kerning, atlas, etc.).
// -------------------------------------------------------------
if (!m_buffer.empty())
gfx.drawString(m_buffer.text(), { textX, textY }, getTextColor(), getFontSize());
// -------------------------------------------------------------
// 8. Draw the cursor. Only when focused and the blink is in the
// "on" phase. The cursor renders ON TOP of everything else so
// it's visible inside selections.
// -------------------------------------------------------------
if (isFocused() && (m_cursorState || !m_cursorBlink))
{
const f32 cx = textX + layout_x_for_byte(m_buffer.cursorByteOffset());
gfx.fillRect(
{ cx, textY, m_cursorWidth, m_layout.lineHeight },
m_cursorColor
);
}
}
void TextEdit::onUpdate(void)
{
m_keyRepeatTimer.update();
m_doubleClickTimer.update();
if (m_cursorBlink)
{
m_cursorBlinkTimer.update();
if (m_cursorBlinkTimer.isDone())
{
m_cursorState = !m_cursorState;
m_cursorBlinkTimer.start();
}
}
// Auto-scroll while drag-selecting past the widget edges.
if (m_mouseSelecting)
{
const auto bounds = getGlobalBounds();
const f32 contentLeft = m_textPadding.left;
const f32 contentRight = bounds.w - m_textPadding.right;
// Only act when mouse is outside the content area horizontally.
if (m_lastMouseLocalX < contentLeft || m_lastMouseLocalX > contentRight)
{
// Use the same hit-test as drag, but it'll naturally clamp to
// start/end of buffer when the mouse is way outside. The
// setCursorByteOffset call will trigger ensure_cursor_visible
// on the next draw, which scrolls the view to follow.
const u32 hit = byte_offset_for_pixel_x(m_lastMouseLocalX);
m_buffer.setCursorByteOffset(hit, true);
}
}
}
void TextEdit::onFocusGained(const Event& event)
{
}
void TextEdit::onFocusLost(const Event& event)
{
}
void TextEdit::onTextEntered(const Event& event)
{
auto& data = *event.keyboard;
if (!m_charFilter || m_charFilter->isValidChar(data.text))
{
if (m_lastChar == data.text && m_keyRepeatTimer.isCounting())
return;
m_buffer.insertText(data.text);
m_lastChar = data.text;
m_keyRepeatTimer.start();
if (m_cursorBlink)
m_cursorBlinkTimer.start();
m_cursorState = true;
}
}
void TextEdit::onKeyPressed(const Event& event)
{
auto& data = *event.keyboard;
if (m_lastKeyCode == data.keyCode && m_keyRepeatTimer.isCounting())
return;
if (data.keyCode == KeyCode::Backspace)
{
m_keyRepeatTimer.start();
m_lastKeyCode = data.keyCode;
m_buffer.backspace();
}
else if (data.keyCode == KeyCode::Left)
{
m_keyRepeatTimer.start();
m_lastKeyCode = data.keyCode;
m_buffer.moveLeft();
}
else if (data.keyCode == KeyCode::Right)
{
m_keyRepeatTimer.start();
m_lastKeyCode = data.keyCode;
m_buffer.moveRight();
}
else if (data.keyCode == KeyCode::Return)
{
m_keyRepeatTimer.start();
m_lastKeyCode = data.keyCode;
if (callback_onActionPerformed)
callback_onActionPerformed(event);
}
}
void TextEdit::onKeyReleased(const Event& event)
{
}
void TextEdit::onMousePressed(const Event& event)
{
auto& data = *event.mouse;
const f32 local_x = data.position_x - getGlobalBounds().x;
const u32 hit = byte_offset_for_pixel_x(local_x);
// Double-click detection: a second press within the timer window AND
// close enough to the last press position counts as a double-click.
// The position check matters — without it, two presses far apart in
// a long document would falsely trigger word selection.
constexpr f32 doubleClickRadius = 5.0f;
const bool isDoubleClick =
m_lastClickValid
&& m_doubleClickTimer.isCounting()
&& std::abs(local_x - m_lastClickLocalX) < doubleClickRadius;
if (isDoubleClick)
{
// Word selection. selectWordAt sets both anchor and cursor;
// we mark the drag flag so subsequent drag motion extends the
// selection by word — though we'll keep the simple implementation
// for now where dragging after a double-click extends by character.
m_buffer.selectWordAt(hit);
m_mouseSelecting = true;
m_lastClickValid = false; // Don't triple-click into something weird.
}
else
{
m_buffer.setCursorByteOffset(hit);
m_mouseSelecting = true;
m_lastClickLocalX = local_x;
m_lastMouseLocalX = local_x;
m_lastClickValid = true;
m_doubleClickTimer.start();
}
if (m_cursorBlink) m_cursorBlinkTimer.start();
m_cursorState = true;
}
void TextEdit::onMouseReleased(const Event& event)
{
m_mouseSelecting = false;
}
void TextEdit::onMouseDragged(const Event& event)
{
if (!m_mouseSelecting) return;
auto& data = *event.mouse;
const f32 local_x = data.position_x - getGlobalBounds().x;
const u32 hit = byte_offset_for_pixel_x(local_x);
// Extend selection to the dragged-to position. The anchor was set by
// setCursorByteOffset(hit) in onMousePressed and stays put.
m_buffer.setCursorByteOffset(hit, true);
m_lastMouseLocalX = local_x;
if (m_cursorBlink) m_cursorBlinkTimer.start();
m_cursorState = true;
}
void TextEdit::rebuild_layout(BasicRenderer2D& gfx)
{
m_layout.byteOffsets.clear();
m_layout.xPositions.clear();
m_layout.totalWidth = 0.0f;
// Empty buffer: a single entry for "x=0 at byte 0" so the cursor
// still has a position to render at.
if (m_buffer.empty())
{
m_layout.lineHeight = gfx.getStringDimensions(" ", getFontSize()).y;
m_layout.byteOffsets.push_back(0);
m_layout.xPositions.push_back(0.0f);
m_layout.dirty = false;
return;
}
// One call to the renderer. Returns one Vec2 per codepoint:
// .x = advance (incl. kerning) contributed by that glyph
// .y = glyph height (same for all glyphs at this font/size)
const stdvec<Vec2> perChar =
gfx.getStringDimensionsPerCharacter(m_buffer.text(), getFontSize());
if (perChar.empty())
{
// Defensive: shouldn't happen with non-empty text, but if the
// font failed to load we still want a valid layout.
m_layout.lineHeight = 0.0f;
m_layout.byteOffsets.push_back(0);
m_layout.xPositions.push_back(0.0f);
m_layout.dirty = false;
return;
}
m_layout.lineHeight = perChar[0].y;
// Walk both the per-character advances AND the buffer's UTF-8 bytes
// in lockstep. Since processString emits exactly one glyph per
// codepoint, the i-th advance corresponds to the codepoint that
// starts at the i-th byte boundary.
const ostd::cpp_string& s = m_buffer.text().cpp_str();
f32 x = 0.0f;
u32 byte_pos = 0;
size_t glyph_idx = 0;
// First entry: position before any glyph.
m_layout.byteOffsets.push_back(0);
m_layout.xPositions.push_back(0.0f);
while (byte_pos < s.size() && glyph_idx < perChar.size())
{
x += perChar[glyph_idx].x;
byte_pos = String::utf8::next_codepoint_start(s, byte_pos);
m_layout.byteOffsets.push_back(byte_pos);
m_layout.xPositions.push_back(x);
glyph_idx++;
}
m_layout.totalWidth = x;
m_layout.dirty = false;
}
f32 TextEdit::layout_x_for_byte(u32 byte_offset) const
{
// Binary search for the entry. Layout invariant: byteOffsets is
// strictly increasing, so std::lower_bound gives us the right index.
const auto& v = m_layout.byteOffsets;
if (v.empty()) return 0.0f;
if (byte_offset >= v.back()) return m_layout.xPositions.back();
auto it = std::lower_bound(v.begin(), v.end(), byte_offset);
const size_t idx = std::distance(v.begin(), it);
return m_layout.xPositions[idx];
}
void TextEdit::ensure_cursor_visible(const Rectangle& bounds)
{
const f32 contentW = bounds.w - m_textPadding.left - m_textPadding.right;
if (contentW <= 0.0f) return;
const f32 cursorX = layout_x_for_byte(m_buffer.cursorByteOffset());
const f32 totalW = m_layout.totalWidth;
// Margin: how far the cursor should stay from the visible edges.
// ~2 character widths feels right; using line height as a proxy works
// well across font sizes since char width scales similarly.
// This single-sourced value also caps itself at 25% of contentW so it
// stays sensible even in very narrow widgets.
const f32 marginRaw = m_layout.lineHeight * 0.6f;
const f32 margin = std::min(marginRaw, contentW * 0.25f);
// Case 1: all the text fits. No scroll needed.
if (totalW <= contentW)
{
m_scrollX = 0.0f;
return;
}
// Case 2: text overflows. Apply margin-aware scrolling.
//
// The visible window in document space is [m_scrollX, m_scrollX + contentW).
// We want cursorX to satisfy:
// m_scrollX + margin <= cursorX <= m_scrollX + contentW - margin
// — except clamped at the document edges.
// If cursor is too close to the left edge of the visible window, scroll
// left so there's `margin` of text visible to its left.
if (cursorX < m_scrollX + margin)
m_scrollX = cursorX - margin;
// If cursor is too close to the right edge, scroll right so there's
// `margin` of room to its right.
else if (cursorX > m_scrollX + contentW - margin)
m_scrollX = cursorX - contentW + margin;
// Clamp scrollX to valid range. The lower bound is 0 (don't show empty
// space to the left of the text). The upper bound is totalW - contentW
// (don't show empty space to the right of the text — i.e., the right
// edge of the text aligns with the right edge of the visible area).
//
// This upper clamp is what produces the "right-anchored when at end"
// behavior automatically: when cursor is at end, the margin formula
// wants to scroll past the end of the text, but we clamp it back so
// the text's right edge sits at the visible area's right edge.
if (m_scrollX < 0.0f)
m_scrollX = 0.0f;
if (m_scrollX > totalW - contentW)
m_scrollX = totalW - contentW;
}
u32 TextEdit::byte_offset_for_pixel_x(f32 widget_local_x) const
{
const f32 doc_x = widget_local_x - m_textPadding.left + m_scrollX;
const auto& xs = m_layout.xPositions;
const auto& bs = m_layout.byteOffsets;
if (xs.empty()) return 0;
if (doc_x <= xs.front()) return bs.front();
if (doc_x >= xs.back()) return bs.back();
auto it = std::upper_bound(xs.begin(), xs.end(), doc_x);
const size_t hi = std::distance(xs.begin(), it);
const size_t lo = hi - 1;
const f32 distLo = doc_x - xs[lo];
const f32 distHi = xs[hi] - doc_x;
return cast<u32>((distLo <= distHi) ? bs[lo] : bs[hi]);
}
} }
} }

View file

@ -20,11 +20,77 @@
#pragma once #pragma once
#include "Widget.hpp" #include <ogfx/gui/widgets/Widget.hpp>
#include <ostd/string/TextBuffer.hpp>
#include <ostd/utils/Time.hpp>
namespace ostd namespace ogfx
{ {
namespace gui namespace gui
{ {
class TextEdit : public Widget
{
public: class CharacterFilter
{
public:
inline virtual bool isValidChar(const String& utf8) { return true; }
};
public: struct TextPadding
{
f32 left { 6.0f };
f32 right { 6.0f };
};
public: struct GlyphLayout
{
stdvec<u32> byteOffsets; // size N+1: byte offset before each codepoint, plus end
stdvec<f32> xPositions; // size N+1: pixel x before each codepoint, plus end-x
f32 totalWidth { 0 };
f32 lineHeight { 0 };
bool dirty { true };
};
public:
inline TextEdit(Window& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
TextEdit& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onUpdate(void) override;
void onFocusGained(const Event& event) override;
void onFocusLost(const Event& event) override;
void onTextEntered(const Event& event) override;
void onKeyPressed(const Event& event) override;
void onKeyReleased(const Event& event) override;
void onMousePressed(const Event& event) override;
void onMouseReleased(const Event& event) override;
void onMouseDragged(const Event& event) override;
inline void setText(const String& text) { m_buffer.setText(text); }
private:
void rebuild_layout(BasicRenderer2D& gfx);
void ensure_cursor_visible(const Rectangle& bounds);
f32 layout_x_for_byte(u32 byte_offset) const;
u32 byte_offset_for_pixel_x(f32 widget_local_x) const;
private:
ostd::TextBuffer m_buffer { "", true };
ostd::BasicCounter m_cursorBlinkTimer { 30 };
ostd::BasicCounter m_keyRepeatTimer { 1 };
ostd::BasicCounter m_doubleClickTimer { 24 }; // ~400ms at 60fps
f32 m_lastClickLocalX { 0.0f };
bool m_lastClickValid { false };
bool m_mouseSelecting { false };
f32 m_lastMouseLocalX { 0.0f };
CharacterFilter* m_charFilter { nullptr };
String m_lastChar { "" };
i32 m_lastKeyCode { 0 };
bool m_cursorBlink { true };
bool m_cursorState { false };
f32 m_cursorWidth { 2 };
Color m_cursorColor { Colors::Red };
GlyphLayout m_layout;
f32 m_scrollX { 0.0f };
TextPadding m_textPadding;
Color m_selectionColor { 60, 110, 200, 200 }; // bluish, semi-transparent
};
} }
} }

View file

@ -1,21 +1,21 @@
/* /*
OmniaFramework - A collection of useful functionality OmniaFramework - A collection of useful functionality
Copyright (C) 2026 OmniaX-Dev Copyright (C) 2026 OmniaX-Dev
This file is part of OmniaFramework. This file is part of OmniaFramework.
OmniaFramework is free software: you can redistribute it and/or modify OmniaFramework is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
OmniaFramework is distributed in the hope that it will be useful, OmniaFramework is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>. along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>.
*/ */
#pragma once #pragma once
@ -26,261 +26,261 @@ namespace ogfx
{ {
struct KeyCode struct KeyCode
{ {
inline static constexpr u32 Unknown { SDLK_UNKNOWN }; inline static constexpr u32 Unknown { SDLK_UNKNOWN };
inline static constexpr u32 Return { SDLK_RETURN }; inline static constexpr u32 Return { SDLK_RETURN };
inline static constexpr u32 Escape { SDLK_ESCAPE }; inline static constexpr u32 Escape { SDLK_ESCAPE };
inline static constexpr u32 Backspace { SDLK_BACKSPACE }; inline static constexpr u32 Backspace { SDLK_BACKSPACE };
inline static constexpr u32 Tab { SDLK_TAB }; inline static constexpr u32 Tab { SDLK_TAB };
inline static constexpr u32 Space { SDLK_SPACE }; inline static constexpr u32 Space { SDLK_SPACE };
inline static constexpr u32 Exclaim { SDLK_EXCLAIM }; inline static constexpr u32 Exclaim { SDLK_EXCLAIM };
inline static constexpr u32 Dblapostrophe { SDLK_DBLAPOSTROPHE }; inline static constexpr u32 Dblapostrophe { SDLK_DBLAPOSTROPHE };
inline static constexpr u32 Hash { SDLK_HASH }; inline static constexpr u32 Hash { SDLK_HASH };
inline static constexpr u32 Dollar { SDLK_DOLLAR }; inline static constexpr u32 Dollar { SDLK_DOLLAR };
inline static constexpr u32 Percent { SDLK_PERCENT }; inline static constexpr u32 Percent { SDLK_PERCENT };
inline static constexpr u32 Ampersand { SDLK_AMPERSAND }; inline static constexpr u32 Ampersand { SDLK_AMPERSAND };
inline static constexpr u32 Apostrophe { SDLK_APOSTROPHE }; inline static constexpr u32 Apostrophe { SDLK_APOSTROPHE };
inline static constexpr u32 Leftparen { SDLK_LEFTPAREN }; inline static constexpr u32 Leftparen { SDLK_LEFTPAREN };
inline static constexpr u32 Rightparen { SDLK_RIGHTPAREN }; inline static constexpr u32 Rightparen { SDLK_RIGHTPAREN };
inline static constexpr u32 Asterisk { SDLK_ASTERISK }; inline static constexpr u32 Asterisk { SDLK_ASTERISK };
inline static constexpr u32 Plus { SDLK_PLUS }; inline static constexpr u32 Plus { SDLK_PLUS };
inline static constexpr u32 Comma { SDLK_COMMA }; inline static constexpr u32 Comma { SDLK_COMMA };
inline static constexpr u32 Minus { SDLK_MINUS }; inline static constexpr u32 Minus { SDLK_MINUS };
inline static constexpr u32 Period { SDLK_PERIOD }; inline static constexpr u32 Period { SDLK_PERIOD };
inline static constexpr u32 Slash { SDLK_SLASH }; inline static constexpr u32 Slash { SDLK_SLASH };
inline static constexpr u32 Num0 { SDLK_0 }; inline static constexpr u32 Num0 { SDLK_0 };
inline static constexpr u32 Num1 { SDLK_1 }; inline static constexpr u32 Num1 { SDLK_1 };
inline static constexpr u32 Num2 { SDLK_2 }; inline static constexpr u32 Num2 { SDLK_2 };
inline static constexpr u32 Num3 { SDLK_3 }; inline static constexpr u32 Num3 { SDLK_3 };
inline static constexpr u32 Num4 { SDLK_4 }; inline static constexpr u32 Num4 { SDLK_4 };
inline static constexpr u32 Num5 { SDLK_5 }; inline static constexpr u32 Num5 { SDLK_5 };
inline static constexpr u32 Num6 { SDLK_6 }; inline static constexpr u32 Num6 { SDLK_6 };
inline static constexpr u32 Num7 { SDLK_7 }; inline static constexpr u32 Num7 { SDLK_7 };
inline static constexpr u32 Num8 { SDLK_8 }; inline static constexpr u32 Num8 { SDLK_8 };
inline static constexpr u32 Num9 { SDLK_9 }; inline static constexpr u32 Num9 { SDLK_9 };
inline static constexpr u32 Colon { SDLK_COLON }; inline static constexpr u32 Colon { SDLK_COLON };
inline static constexpr u32 Semicolon { SDLK_SEMICOLON }; inline static constexpr u32 Semicolon { SDLK_SEMICOLON };
inline static constexpr u32 Less { SDLK_LESS }; inline static constexpr u32 Less { SDLK_LESS };
inline static constexpr u32 Equals { SDLK_EQUALS }; inline static constexpr u32 Equals { SDLK_EQUALS };
inline static constexpr u32 Greater { SDLK_GREATER }; inline static constexpr u32 Greater { SDLK_GREATER };
inline static constexpr u32 Question { SDLK_QUESTION }; inline static constexpr u32 Question { SDLK_QUESTION };
inline static constexpr u32 At { SDLK_AT }; inline static constexpr u32 At { SDLK_AT };
inline static constexpr u32 LeftBracket { SDLK_LEFTBRACKET }; inline static constexpr u32 LeftBracket { SDLK_LEFTBRACKET };
inline static constexpr u32 Backslash { SDLK_BACKSLASH }; inline static constexpr u32 Backslash { SDLK_BACKSLASH };
inline static constexpr u32 RightBracket { SDLK_RIGHTBRACKET }; inline static constexpr u32 RightBracket { SDLK_RIGHTBRACKET };
inline static constexpr u32 Caret { SDLK_CARET }; inline static constexpr u32 Caret { SDLK_CARET };
inline static constexpr u32 Underscore { SDLK_UNDERSCORE }; inline static constexpr u32 Underscore { SDLK_UNDERSCORE };
inline static constexpr u32 Grave { SDLK_GRAVE }; inline static constexpr u32 Grave { SDLK_GRAVE };
inline static constexpr u32 A { SDLK_A }; inline static constexpr u32 A { SDLK_A };
inline static constexpr u32 B { SDLK_B }; inline static constexpr u32 B { SDLK_B };
inline static constexpr u32 C { SDLK_C }; inline static constexpr u32 C { SDLK_C };
inline static constexpr u32 D { SDLK_D }; inline static constexpr u32 D { SDLK_D };
inline static constexpr u32 E { SDLK_E }; inline static constexpr u32 E { SDLK_E };
inline static constexpr u32 F { SDLK_F }; inline static constexpr u32 F { SDLK_F };
inline static constexpr u32 G { SDLK_G }; inline static constexpr u32 G { SDLK_G };
inline static constexpr u32 H { SDLK_H }; inline static constexpr u32 H { SDLK_H };
inline static constexpr u32 I { SDLK_I }; inline static constexpr u32 I { SDLK_I };
inline static constexpr u32 J { SDLK_J }; inline static constexpr u32 J { SDLK_J };
inline static constexpr u32 K { SDLK_K }; inline static constexpr u32 K { SDLK_K };
inline static constexpr u32 L { SDLK_L }; inline static constexpr u32 L { SDLK_L };
inline static constexpr u32 M { SDLK_M }; inline static constexpr u32 M { SDLK_M };
inline static constexpr u32 N { SDLK_N }; inline static constexpr u32 N { SDLK_N };
inline static constexpr u32 O { SDLK_O }; inline static constexpr u32 O { SDLK_O };
inline static constexpr u32 P { SDLK_P }; inline static constexpr u32 P { SDLK_P };
inline static constexpr u32 Q { SDLK_Q }; inline static constexpr u32 Q { SDLK_Q };
inline static constexpr u32 R { SDLK_R }; inline static constexpr u32 R { SDLK_R };
inline static constexpr u32 S { SDLK_S }; inline static constexpr u32 S { SDLK_S };
inline static constexpr u32 T { SDLK_T }; inline static constexpr u32 T { SDLK_T };
inline static constexpr u32 U { SDLK_U }; inline static constexpr u32 U { SDLK_U };
inline static constexpr u32 V { SDLK_V }; inline static constexpr u32 V { SDLK_V };
inline static constexpr u32 W { SDLK_W }; inline static constexpr u32 W { SDLK_W };
inline static constexpr u32 X { SDLK_X }; inline static constexpr u32 X { SDLK_X };
inline static constexpr u32 Y { SDLK_Y }; inline static constexpr u32 Y { SDLK_Y };
inline static constexpr u32 Z { SDLK_Z }; inline static constexpr u32 Z { SDLK_Z };
inline static constexpr u32 Leftbrace { SDLK_LEFTBRACE }; inline static constexpr u32 Leftbrace { SDLK_LEFTBRACE };
inline static constexpr u32 Pipe { SDLK_PIPE }; inline static constexpr u32 Pipe { SDLK_PIPE };
inline static constexpr u32 Rightbrace { SDLK_RIGHTBRACE }; inline static constexpr u32 Rightbrace { SDLK_RIGHTBRACE };
inline static constexpr u32 Tilde { SDLK_TILDE }; inline static constexpr u32 Tilde { SDLK_TILDE };
inline static constexpr u32 Delete { SDLK_DELETE }; inline static constexpr u32 Delete { SDLK_DELETE };
inline static constexpr u32 Plusminus { SDLK_PLUSMINUS }; inline static constexpr u32 Plusminus { SDLK_PLUSMINUS };
inline static constexpr u32 Capslock { SDLK_CAPSLOCK }; inline static constexpr u32 Capslock { SDLK_CAPSLOCK };
inline static constexpr u32 F1 { SDLK_F1 }; inline static constexpr u32 F1 { SDLK_F1 };
inline static constexpr u32 F2 { SDLK_F2 }; inline static constexpr u32 F2 { SDLK_F2 };
inline static constexpr u32 F3 { SDLK_F3 }; inline static constexpr u32 F3 { SDLK_F3 };
inline static constexpr u32 F4 { SDLK_F4 }; inline static constexpr u32 F4 { SDLK_F4 };
inline static constexpr u32 F5 { SDLK_F5 }; inline static constexpr u32 F5 { SDLK_F5 };
inline static constexpr u32 F6 { SDLK_F6 }; inline static constexpr u32 F6 { SDLK_F6 };
inline static constexpr u32 F7 { SDLK_F7 }; inline static constexpr u32 F7 { SDLK_F7 };
inline static constexpr u32 F8 { SDLK_F8 }; inline static constexpr u32 F8 { SDLK_F8 };
inline static constexpr u32 F9 { SDLK_F9 }; inline static constexpr u32 F9 { SDLK_F9 };
inline static constexpr u32 F10 { SDLK_F10 }; inline static constexpr u32 F10 { SDLK_F10 };
inline static constexpr u32 F11 { SDLK_F11 }; inline static constexpr u32 F11 { SDLK_F11 };
inline static constexpr u32 F12 { SDLK_F12 }; inline static constexpr u32 F12 { SDLK_F12 };
inline static constexpr u32 Printscreen { SDLK_PRINTSCREEN }; inline static constexpr u32 Printscreen { SDLK_PRINTSCREEN };
inline static constexpr u32 Scrolllock { SDLK_SCROLLLOCK }; inline static constexpr u32 Scrolllock { SDLK_SCROLLLOCK };
inline static constexpr u32 Pause { SDLK_PAUSE }; inline static constexpr u32 Pause { SDLK_PAUSE };
inline static constexpr u32 Insert { SDLK_INSERT }; inline static constexpr u32 Insert { SDLK_INSERT };
inline static constexpr u32 Home { SDLK_HOME }; inline static constexpr u32 Home { SDLK_HOME };
inline static constexpr u32 Pageup { SDLK_PAGEUP }; inline static constexpr u32 Pageup { SDLK_PAGEUP };
inline static constexpr u32 End { SDLK_END }; inline static constexpr u32 End { SDLK_END };
inline static constexpr u32 Pagedown { SDLK_PAGEDOWN }; inline static constexpr u32 Pagedown { SDLK_PAGEDOWN };
inline static constexpr u32 Right { SDLK_RIGHT }; inline static constexpr u32 Right { SDLK_RIGHT };
inline static constexpr u32 Left { SDLK_LEFT }; inline static constexpr u32 Left { SDLK_LEFT };
inline static constexpr u32 Down { SDLK_DOWN }; inline static constexpr u32 Down { SDLK_DOWN };
inline static constexpr u32 Up { SDLK_UP }; inline static constexpr u32 Up { SDLK_UP };
inline static constexpr u32 Numlockclear { SDLK_NUMLOCKCLEAR }; inline static constexpr u32 Numlockclear { SDLK_NUMLOCKCLEAR };
inline static constexpr u32 KpDivide { SDLK_KP_DIVIDE }; inline static constexpr u32 KpDivide { SDLK_KP_DIVIDE };
inline static constexpr u32 KpMultiply { SDLK_KP_MULTIPLY }; inline static constexpr u32 KpMultiply { SDLK_KP_MULTIPLY };
inline static constexpr u32 KpMinus { SDLK_KP_MINUS }; inline static constexpr u32 KpMinus { SDLK_KP_MINUS };
inline static constexpr u32 KpPlus { SDLK_KP_PLUS }; inline static constexpr u32 KpPlus { SDLK_KP_PLUS };
inline static constexpr u32 KpEnter { SDLK_KP_ENTER }; inline static constexpr u32 KpEnter { SDLK_KP_ENTER };
inline static constexpr u32 Kp1 { SDLK_KP_1 }; inline static constexpr u32 Kp1 { SDLK_KP_1 };
inline static constexpr u32 Kp2 { SDLK_KP_2 }; inline static constexpr u32 Kp2 { SDLK_KP_2 };
inline static constexpr u32 Kp3 { SDLK_KP_3 }; inline static constexpr u32 Kp3 { SDLK_KP_3 };
inline static constexpr u32 Kp4 { SDLK_KP_4 }; inline static constexpr u32 Kp4 { SDLK_KP_4 };
inline static constexpr u32 Kp5 { SDLK_KP_5 }; inline static constexpr u32 Kp5 { SDLK_KP_5 };
inline static constexpr u32 Kp6 { SDLK_KP_6 }; inline static constexpr u32 Kp6 { SDLK_KP_6 };
inline static constexpr u32 Kp7 { SDLK_KP_7 }; inline static constexpr u32 Kp7 { SDLK_KP_7 };
inline static constexpr u32 Kp8 { SDLK_KP_8 }; inline static constexpr u32 Kp8 { SDLK_KP_8 };
inline static constexpr u32 Kp9 { SDLK_KP_9 }; inline static constexpr u32 Kp9 { SDLK_KP_9 };
inline static constexpr u32 Kp0 { SDLK_KP_0 }; inline static constexpr u32 Kp0 { SDLK_KP_0 };
inline static constexpr u32 KpPeriod { SDLK_KP_PERIOD }; inline static constexpr u32 KpPeriod { SDLK_KP_PERIOD };
inline static constexpr u32 Application { SDLK_APPLICATION }; inline static constexpr u32 Application { SDLK_APPLICATION };
inline static constexpr u32 Power { SDLK_POWER }; inline static constexpr u32 Power { SDLK_POWER };
inline static constexpr u32 KpEquals { SDLK_KP_EQUALS }; inline static constexpr u32 KpEquals { SDLK_KP_EQUALS };
inline static constexpr u32 F13 { SDLK_F13 }; inline static constexpr u32 F13 { SDLK_F13 };
inline static constexpr u32 F14 { SDLK_F14 }; inline static constexpr u32 F14 { SDLK_F14 };
inline static constexpr u32 F15 { SDLK_F15 }; inline static constexpr u32 F15 { SDLK_F15 };
inline static constexpr u32 F16 { SDLK_F16 }; inline static constexpr u32 F16 { SDLK_F16 };
inline static constexpr u32 F17 { SDLK_F17 }; inline static constexpr u32 F17 { SDLK_F17 };
inline static constexpr u32 F18 { SDLK_F18 }; inline static constexpr u32 F18 { SDLK_F18 };
inline static constexpr u32 F19 { SDLK_F19 }; inline static constexpr u32 F19 { SDLK_F19 };
inline static constexpr u32 F20 { SDLK_F20 }; inline static constexpr u32 F20 { SDLK_F20 };
inline static constexpr u32 F21 { SDLK_F21 }; inline static constexpr u32 F21 { SDLK_F21 };
inline static constexpr u32 F22 { SDLK_F22 }; inline static constexpr u32 F22 { SDLK_F22 };
inline static constexpr u32 F23 { SDLK_F23 }; inline static constexpr u32 F23 { SDLK_F23 };
inline static constexpr u32 F24 { SDLK_F24 }; inline static constexpr u32 F24 { SDLK_F24 };
inline static constexpr u32 Execute { SDLK_EXECUTE }; inline static constexpr u32 Execute { SDLK_EXECUTE };
inline static constexpr u32 Help { SDLK_HELP }; inline static constexpr u32 Help { SDLK_HELP };
inline static constexpr u32 Menu { SDLK_MENU }; inline static constexpr u32 Menu { SDLK_MENU };
inline static constexpr u32 Select { SDLK_SELECT }; inline static constexpr u32 Select { SDLK_SELECT };
inline static constexpr u32 Stop { SDLK_STOP }; inline static constexpr u32 Stop { SDLK_STOP };
inline static constexpr u32 Again { SDLK_AGAIN }; inline static constexpr u32 Again { SDLK_AGAIN };
inline static constexpr u32 Undo { SDLK_UNDO }; inline static constexpr u32 Undo { SDLK_UNDO };
inline static constexpr u32 Cut { SDLK_CUT }; inline static constexpr u32 Cut { SDLK_CUT };
inline static constexpr u32 Copy { SDLK_COPY }; inline static constexpr u32 Copy { SDLK_COPY };
inline static constexpr u32 Paste { SDLK_PASTE }; inline static constexpr u32 Paste { SDLK_PASTE };
inline static constexpr u32 Find { SDLK_FIND }; inline static constexpr u32 Find { SDLK_FIND };
inline static constexpr u32 Mute { SDLK_MUTE }; inline static constexpr u32 Mute { SDLK_MUTE };
inline static constexpr u32 Volumeup { SDLK_VOLUMEUP }; inline static constexpr u32 Volumeup { SDLK_VOLUMEUP };
inline static constexpr u32 Volumedown { SDLK_VOLUMEDOWN }; inline static constexpr u32 Volumedown { SDLK_VOLUMEDOWN };
inline static constexpr u32 KpComma { SDLK_KP_COMMA }; inline static constexpr u32 KpComma { SDLK_KP_COMMA };
inline static constexpr u32 KpEqualsas400 { SDLK_KP_EQUALSAS400 }; inline static constexpr u32 KpEqualsas400 { SDLK_KP_EQUALSAS400 };
inline static constexpr u32 Alterase { SDLK_ALTERASE }; inline static constexpr u32 Alterase { SDLK_ALTERASE };
inline static constexpr u32 Sysreq { SDLK_SYSREQ }; inline static constexpr u32 Sysreq { SDLK_SYSREQ };
inline static constexpr u32 Cancel { SDLK_CANCEL }; inline static constexpr u32 Cancel { SDLK_CANCEL };
inline static constexpr u32 Clear { SDLK_CLEAR }; inline static constexpr u32 Clear { SDLK_CLEAR };
inline static constexpr u32 Prior { SDLK_PRIOR }; inline static constexpr u32 Prior { SDLK_PRIOR };
inline static constexpr u32 Return2 { SDLK_RETURN2 }; inline static constexpr u32 Return2 { SDLK_RETURN2 };
inline static constexpr u32 Separator { SDLK_SEPARATOR }; inline static constexpr u32 Separator { SDLK_SEPARATOR };
inline static constexpr u32 Out { SDLK_OUT }; inline static constexpr u32 Out { SDLK_OUT };
inline static constexpr u32 Oper { SDLK_OPER }; inline static constexpr u32 Oper { SDLK_OPER };
inline static constexpr u32 Clearagain { SDLK_CLEARAGAIN }; inline static constexpr u32 Clearagain { SDLK_CLEARAGAIN };
inline static constexpr u32 Crsel { SDLK_CRSEL }; inline static constexpr u32 Crsel { SDLK_CRSEL };
inline static constexpr u32 Exsel { SDLK_EXSEL }; inline static constexpr u32 Exsel { SDLK_EXSEL };
inline static constexpr u32 Kp00 { SDLK_KP_00 }; inline static constexpr u32 Kp00 { SDLK_KP_00 };
inline static constexpr u32 Kp000 { SDLK_KP_000 }; inline static constexpr u32 Kp000 { SDLK_KP_000 };
inline static constexpr u32 Thousandsseparator { SDLK_THOUSANDSSEPARATOR }; inline static constexpr u32 Thousandsseparator { SDLK_THOUSANDSSEPARATOR };
inline static constexpr u32 Decimalseparator { SDLK_DECIMALSEPARATOR }; inline static constexpr u32 Decimalseparator { SDLK_DECIMALSEPARATOR };
inline static constexpr u32 Currencyunit { SDLK_CURRENCYUNIT }; inline static constexpr u32 Currencyunit { SDLK_CURRENCYUNIT };
inline static constexpr u32 Currencysubunit { SDLK_CURRENCYSUBUNIT }; inline static constexpr u32 Currencysubunit { SDLK_CURRENCYSUBUNIT };
inline static constexpr u32 KpLeftparen { SDLK_KP_LEFTPAREN }; inline static constexpr u32 KpLeftparen { SDLK_KP_LEFTPAREN };
inline static constexpr u32 KpRightparen { SDLK_KP_RIGHTPAREN }; inline static constexpr u32 KpRightparen { SDLK_KP_RIGHTPAREN };
inline static constexpr u32 KpLeftbrace { SDLK_KP_LEFTBRACE }; inline static constexpr u32 KpLeftbrace { SDLK_KP_LEFTBRACE };
inline static constexpr u32 KpRightbrace { SDLK_KP_RIGHTBRACE }; inline static constexpr u32 KpRightbrace { SDLK_KP_RIGHTBRACE };
inline static constexpr u32 KpTab { SDLK_KP_TAB }; inline static constexpr u32 KpTab { SDLK_KP_TAB };
inline static constexpr u32 KpBackspace { SDLK_KP_BACKSPACE }; inline static constexpr u32 KpBackspace { SDLK_KP_BACKSPACE };
inline static constexpr u32 KpA { SDLK_KP_A }; inline static constexpr u32 KpA { SDLK_KP_A };
inline static constexpr u32 KpB { SDLK_KP_B }; inline static constexpr u32 KpB { SDLK_KP_B };
inline static constexpr u32 KpC { SDLK_KP_C }; inline static constexpr u32 KpC { SDLK_KP_C };
inline static constexpr u32 KpD { SDLK_KP_D }; inline static constexpr u32 KpD { SDLK_KP_D };
inline static constexpr u32 KpE { SDLK_KP_E }; inline static constexpr u32 KpE { SDLK_KP_E };
inline static constexpr u32 KpF { SDLK_KP_F }; inline static constexpr u32 KpF { SDLK_KP_F };
inline static constexpr u32 KpXor { SDLK_KP_XOR }; inline static constexpr u32 KpXor { SDLK_KP_XOR };
inline static constexpr u32 KpPower { SDLK_KP_POWER }; inline static constexpr u32 KpPower { SDLK_KP_POWER };
inline static constexpr u32 KpPercent { SDLK_KP_PERCENT }; inline static constexpr u32 KpPercent { SDLK_KP_PERCENT };
inline static constexpr u32 KpLess { SDLK_KP_LESS }; inline static constexpr u32 KpLess { SDLK_KP_LESS };
inline static constexpr u32 KpGreater { SDLK_KP_GREATER }; inline static constexpr u32 KpGreater { SDLK_KP_GREATER };
inline static constexpr u32 KpAmpersand { SDLK_KP_AMPERSAND }; inline static constexpr u32 KpAmpersand { SDLK_KP_AMPERSAND };
inline static constexpr u32 KpDblampersand { SDLK_KP_DBLAMPERSAND }; inline static constexpr u32 KpDblampersand { SDLK_KP_DBLAMPERSAND };
inline static constexpr u32 KpVerticalbar { SDLK_KP_VERTICALBAR }; inline static constexpr u32 KpVerticalbar { SDLK_KP_VERTICALBAR };
inline static constexpr u32 KpDblverticalbar { SDLK_KP_DBLVERTICALBAR }; inline static constexpr u32 KpDblverticalbar { SDLK_KP_DBLVERTICALBAR };
inline static constexpr u32 KpColon { SDLK_KP_COLON }; inline static constexpr u32 KpColon { SDLK_KP_COLON };
inline static constexpr u32 KpHash { SDLK_KP_HASH }; inline static constexpr u32 KpHash { SDLK_KP_HASH };
inline static constexpr u32 KpSpace { SDLK_KP_SPACE }; inline static constexpr u32 KpSpace { SDLK_KP_SPACE };
inline static constexpr u32 KpAt { SDLK_KP_AT }; inline static constexpr u32 KpAt { SDLK_KP_AT };
inline static constexpr u32 KpExclam { SDLK_KP_EXCLAM }; inline static constexpr u32 KpExclam { SDLK_KP_EXCLAM };
inline static constexpr u32 KpMemstore { SDLK_KP_MEMSTORE }; inline static constexpr u32 KpMemstore { SDLK_KP_MEMSTORE };
inline static constexpr u32 KpMemrecall { SDLK_KP_MEMRECALL }; inline static constexpr u32 KpMemrecall { SDLK_KP_MEMRECALL };
inline static constexpr u32 KpMemclear { SDLK_KP_MEMCLEAR }; inline static constexpr u32 KpMemclear { SDLK_KP_MEMCLEAR };
inline static constexpr u32 KpMemadd { SDLK_KP_MEMADD }; inline static constexpr u32 KpMemadd { SDLK_KP_MEMADD };
inline static constexpr u32 KpMemsubtract { SDLK_KP_MEMSUBTRACT }; inline static constexpr u32 KpMemsubtract { SDLK_KP_MEMSUBTRACT };
inline static constexpr u32 KpMemmultiply { SDLK_KP_MEMMULTIPLY }; inline static constexpr u32 KpMemmultiply { SDLK_KP_MEMMULTIPLY };
inline static constexpr u32 KpMemdivide { SDLK_KP_MEMDIVIDE }; inline static constexpr u32 KpMemdivide { SDLK_KP_MEMDIVIDE };
inline static constexpr u32 KpPlusminus { SDLK_KP_PLUSMINUS }; inline static constexpr u32 KpPlusminus { SDLK_KP_PLUSMINUS };
inline static constexpr u32 KpClear { SDLK_KP_CLEAR }; inline static constexpr u32 KpClear { SDLK_KP_CLEAR };
inline static constexpr u32 KpClearentry { SDLK_KP_CLEARENTRY }; inline static constexpr u32 KpClearentry { SDLK_KP_CLEARENTRY };
inline static constexpr u32 KpBinary { SDLK_KP_BINARY }; inline static constexpr u32 KpBinary { SDLK_KP_BINARY };
inline static constexpr u32 KpOctal { SDLK_KP_OCTAL }; inline static constexpr u32 KpOctal { SDLK_KP_OCTAL };
inline static constexpr u32 KpDecimal { SDLK_KP_DECIMAL }; inline static constexpr u32 KpDecimal { SDLK_KP_DECIMAL };
inline static constexpr u32 KpHexadecimal { SDLK_KP_HEXADECIMAL }; inline static constexpr u32 KpHexadecimal { SDLK_KP_HEXADECIMAL };
inline static constexpr u32 Lctrl { SDLK_LCTRL }; inline static constexpr u32 Lctrl { SDLK_LCTRL };
inline static constexpr u32 Lshift { SDLK_LSHIFT }; inline static constexpr u32 Lshift { SDLK_LSHIFT };
inline static constexpr u32 Lalt { SDLK_LALT }; inline static constexpr u32 Lalt { SDLK_LALT };
inline static constexpr u32 Lgui { SDLK_LGUI }; inline static constexpr u32 Lgui { SDLK_LGUI };
inline static constexpr u32 Rctrl { SDLK_RCTRL }; inline static constexpr u32 Rctrl { SDLK_RCTRL };
inline static constexpr u32 Rshift { SDLK_RSHIFT }; inline static constexpr u32 Rshift { SDLK_RSHIFT };
inline static constexpr u32 Ralt { SDLK_RALT }; inline static constexpr u32 Ralt { SDLK_RALT };
inline static constexpr u32 Rgui { SDLK_RGUI }; inline static constexpr u32 Rgui { SDLK_RGUI };
inline static constexpr u32 Mode { SDLK_MODE }; inline static constexpr u32 Mode { SDLK_MODE };
inline static constexpr u32 Sleep { SDLK_SLEEP }; inline static constexpr u32 Sleep { SDLK_SLEEP };
inline static constexpr u32 Wake { SDLK_WAKE }; inline static constexpr u32 Wake { SDLK_WAKE };
inline static constexpr u32 ChannelIncrement { SDLK_CHANNEL_INCREMENT }; inline static constexpr u32 ChannelIncrement { SDLK_CHANNEL_INCREMENT };
inline static constexpr u32 ChannelDecrement { SDLK_CHANNEL_DECREMENT }; inline static constexpr u32 ChannelDecrement { SDLK_CHANNEL_DECREMENT };
inline static constexpr u32 MediaPlay { SDLK_MEDIA_PLAY }; inline static constexpr u32 MediaPlay { SDLK_MEDIA_PLAY };
inline static constexpr u32 MediaPause { SDLK_MEDIA_PAUSE }; inline static constexpr u32 MediaPause { SDLK_MEDIA_PAUSE };
inline static constexpr u32 MediaRecord { SDLK_MEDIA_RECORD }; inline static constexpr u32 MediaRecord { SDLK_MEDIA_RECORD };
inline static constexpr u32 MediaFastForward { SDLK_MEDIA_FAST_FORWARD }; inline static constexpr u32 MediaFastForward { SDLK_MEDIA_FAST_FORWARD };
inline static constexpr u32 MediaRewind { SDLK_MEDIA_REWIND }; inline static constexpr u32 MediaRewind { SDLK_MEDIA_REWIND };
inline static constexpr u32 MediaNextTrack { SDLK_MEDIA_NEXT_TRACK }; inline static constexpr u32 MediaNextTrack { SDLK_MEDIA_NEXT_TRACK };
inline static constexpr u32 MediaPreviousTrack { SDLK_MEDIA_PREVIOUS_TRACK }; inline static constexpr u32 MediaPreviousTrack { SDLK_MEDIA_PREVIOUS_TRACK };
inline static constexpr u32 MediaStop { SDLK_MEDIA_STOP }; inline static constexpr u32 MediaStop { SDLK_MEDIA_STOP };
inline static constexpr u32 MediaEject { SDLK_MEDIA_EJECT }; inline static constexpr u32 MediaEject { SDLK_MEDIA_EJECT };
inline static constexpr u32 MediaPlayPause { SDLK_MEDIA_PLAY_PAUSE }; inline static constexpr u32 MediaPlayPause { SDLK_MEDIA_PLAY_PAUSE };
inline static constexpr u32 MediaSelect { SDLK_MEDIA_SELECT }; inline static constexpr u32 MediaSelect { SDLK_MEDIA_SELECT };
inline static constexpr u32 AcNew { SDLK_AC_NEW }; inline static constexpr u32 AcNew { SDLK_AC_NEW };
inline static constexpr u32 AcOpen { SDLK_AC_OPEN }; inline static constexpr u32 AcOpen { SDLK_AC_OPEN };
inline static constexpr u32 AcClose { SDLK_AC_CLOSE }; inline static constexpr u32 AcClose { SDLK_AC_CLOSE };
inline static constexpr u32 AcExit { SDLK_AC_EXIT }; inline static constexpr u32 AcExit { SDLK_AC_EXIT };
inline static constexpr u32 AcSave { SDLK_AC_SAVE }; inline static constexpr u32 AcSave { SDLK_AC_SAVE };
inline static constexpr u32 AcPrint { SDLK_AC_PRINT }; inline static constexpr u32 AcPrint { SDLK_AC_PRINT };
inline static constexpr u32 AcProperties { SDLK_AC_PROPERTIES }; inline static constexpr u32 AcProperties { SDLK_AC_PROPERTIES };
inline static constexpr u32 AcSearch { SDLK_AC_SEARCH }; inline static constexpr u32 AcSearch { SDLK_AC_SEARCH };
inline static constexpr u32 AcHome { SDLK_AC_HOME }; inline static constexpr u32 AcHome { SDLK_AC_HOME };
inline static constexpr u32 AcBack { SDLK_AC_BACK }; inline static constexpr u32 AcBack { SDLK_AC_BACK };
inline static constexpr u32 AcForward { SDLK_AC_FORWARD }; inline static constexpr u32 AcForward { SDLK_AC_FORWARD };
inline static constexpr u32 AcStop { SDLK_AC_STOP }; inline static constexpr u32 AcStop { SDLK_AC_STOP };
inline static constexpr u32 AcRefresh { SDLK_AC_REFRESH }; inline static constexpr u32 AcRefresh { SDLK_AC_REFRESH };
inline static constexpr u32 AcBookmarks { SDLK_AC_BOOKMARKS }; inline static constexpr u32 AcBookmarks { SDLK_AC_BOOKMARKS };
inline static constexpr u32 Softleft { SDLK_SOFTLEFT }; inline static constexpr u32 Softleft { SDLK_SOFTLEFT };
inline static constexpr u32 Softright { SDLK_SOFTRIGHT }; inline static constexpr u32 Softright { SDLK_SOFTRIGHT };
inline static constexpr u32 Call { SDLK_CALL }; inline static constexpr u32 Call { SDLK_CALL };
inline static constexpr u32 Endcall { SDLK_ENDCALL }; inline static constexpr u32 Endcall { SDLK_ENDCALL };
inline static constexpr u32 LeftTab { SDLK_LEFT_TAB }; inline static constexpr u32 LeftTab { SDLK_LEFT_TAB };
inline static constexpr u32 Level5Shift { SDLK_LEVEL5_SHIFT }; inline static constexpr u32 Level5Shift { SDLK_LEVEL5_SHIFT };
inline static constexpr u32 MultiKeyCompose { SDLK_MULTI_KEY_COMPOSE }; inline static constexpr u32 MultiKeyCompose { SDLK_MULTI_KEY_COMPOSE };
inline static constexpr u32 Lmeta { SDLK_LMETA }; inline static constexpr u32 Lmeta { SDLK_LMETA };
inline static constexpr u32 Rmeta { SDLK_RMETA }; inline static constexpr u32 Rmeta { SDLK_RMETA };
inline static constexpr u32 Lhyper { SDLK_LHYPER }; inline static constexpr u32 Lhyper { SDLK_LHYPER };
inline static constexpr u32 Rhyper { SDLK_RHYPER }; inline static constexpr u32 Rhyper { SDLK_RHYPER };
}; };
} }

View file

@ -325,6 +325,13 @@ namespace ostd
move_cursor_to(cast<u32>(m_buffer.len()), extend); move_cursor_to(cast<u32>(m_buffer.len()), extend);
} }
void TextBuffer::setCursorByteOffset(u32 byte_offset, bool extend)
{
const u32 clamped = clamp_to_codepoint_boundary(byte_offset);
move_cursor_to(clamped, extend);
m_desiredColumn = -1;
}
// ============================================================ // ============================================================
// Mutation // Mutation
@ -602,6 +609,50 @@ namespace ostd
} }
} }
void TextBuffer::selectWordAt(u32 byte_offset)
{
m_desiredColumn = -1;
if (m_buffer.empty()) return;
// Clamp to a valid codepoint boundary first. If we're past end, back up
// to the last codepoint so we have something to classify.
u32 pos = clamp_to_codepoint_boundary(byte_offset);
if (pos >= m_buffer.len()) {
// At end-of-buffer — step back one codepoint to get something to
// classify, then expand from there.
if (m_buffer.len() == 0) return;
pos = String::utf8::prev_codepoint_start(m_buffer.cpp_str(), m_buffer.len());
}
// Expand left and right from `pos`. The trick: prev_word_boundary and
// next_word_boundary are *almost* what we want, but they're designed
// for "skip the run AND following whitespace" semantics. For word
// *selection*, we want just the run.
//
// Easiest: classify the byte at pos, then walk outward in both
// directions while the class matches.
const cpp_string& s = m_buffer.cpp_str();
const WordClass cls = classify_byte(cast<u8>(s[pos]));
// Walk left: advance start backward while previous codepoint is same class.
u32 start = pos;
while (start > 0) {
const u32 prev = String::utf8::prev_codepoint_start(s, start);
if (classify_byte(cast<u8>(s[prev])) != cls) break;
start = prev;
}
// Walk right: advance end forward while current codepoint is same class.
u32 end = pos;
while (end < s.size()) {
if (classify_byte(cast<u8>(s[end])) != cls) break;
end = String::utf8::next_codepoint_start(s, end);
}
selectRange(start, end);
}
u32 TextBuffer::prev_word_boundary(u32 byte_offset) const u32 TextBuffer::prev_word_boundary(u32 byte_offset) const
{ {
const cpp_string& s = m_buffer.cpp_str(); const cpp_string& s = m_buffer.cpp_str();

View file

@ -94,6 +94,7 @@ namespace ostd
String selectedText(void) const; String selectedText(void) const;
void clearSelection(void); void clearSelection(void);
void selectAll(void); void selectAll(void);
void selectWordAt(u32 byte_offset);
void selectRange(u32 start_byte, u32 end_byte); void selectRange(u32 start_byte, u32 end_byte);
// ===================== Cursor movement ==================== // ===================== Cursor movement ====================
@ -108,6 +109,7 @@ namespace ostd
void moveLineEnd(bool extend = false); void moveLineEnd(bool extend = false);
void moveDocumentStart(bool extend = false); void moveDocumentStart(bool extend = false);
void moveDocumentEnd(bool extend = false); void moveDocumentEnd(bool extend = false);
void setCursorByteOffset(u32 byte_offset, bool extend);
// ======================== Mutation ======================== // ======================== Mutation ========================
// All mutators replace the current selection (if any) before // All mutators replace the current selection (if any) before

View file

@ -1,21 +1,21 @@
/* /*
OmniaFramework - A collection of useful functionality OmniaFramework - A collection of useful functionality
Copyright (C) 2026 OmniaX-Dev Copyright (C) 2026 OmniaX-Dev
This file is part of OmniaFramework. This file is part of OmniaFramework.
OmniaFramework is free software: you can redistribute it and/or modify OmniaFramework is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
OmniaFramework is distributed in the hope that it will be useful, OmniaFramework is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>. along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include "utils/Keycodes.hpp" #include "utils/Keycodes.hpp"
@ -29,9 +29,8 @@ class Window : public ogfx::GraphicsWindow
inline Window(void) { } inline Window(void) { }
inline void onInitialize(void) override inline void onInitialize(void) override
{ {
connectSignal(ogfx::gui::RawTextInput::actionEventSignalID);
m_gfx.init(*this); m_gfx.init(*this);
} }
inline void handleSignal(ostd::Signal& signal) override inline void handleSignal(ostd::Signal& signal) override
{ {
@ -71,7 +70,7 @@ i32 main(i32 argc, char** argv)
{ {
Window window; Window window;
window.initialize(800, 600, "OmniaFramework - Test Window"); window.initialize(800, 600, "OmniaFramework - Test Window");
window.setClearColor({ 0, 2 , 15 }); window.setClearColor({ 0, 2 , 15 });
while (window.isRunning()) while (window.isRunning())
{ {

View file

@ -229,6 +229,7 @@ class TestWindow : public Window
t1.addWidget(m_list, { 30, 300 }); t1.addWidget(m_list, { 30, 300 });
t1.addWidget(m_panel2, { 500, 100 }); t1.addWidget(m_panel2, { 500, 100 });
t1.addWidget(m_combo, { 400, 600 }); t1.addWidget(m_combo, { 400, 600 });
t1.addWidget(m_text, { 400, 700 });
t2.setLayout<BoxLayout>(BoxLayout::Orientation::Vertical); t2.setLayout<BoxLayout>(BoxLayout::Orientation::Vertical);
t2.getLayout()->setSpacing(16); t2.getLayout()->setSpacing(16);
@ -390,6 +391,7 @@ class TestWindow : public Window
Label m_cacheHitsLbl { *this }; Label m_cacheHitsLbl { *this };
Label m_cacheMissesLbl { *this }; Label m_cacheMissesLbl { *this };
ComboBox m_combo { *this }; ComboBox m_combo { *this };
TextEdit m_text { *this };
enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted }; enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted };