From a3eb4ba3ccb6bf187a4e38e1f35c5ab9444c74a1 Mon Sep 17 00:00:00 2001 From: OmniaX-Dev Date: Thu, 7 May 2026 06:39:43 +0200 Subject: [PATCH] Started work on TextEdit widget --- build | 17 + extra/DefaultThemeDark.oss | 11 + other/TODO.txt | 17 +- other/build.nr | 2 +- src/ogfx/gui/Events.hpp | 4 +- src/ogfx/gui/RawTextInput.cpp | 36 +-- src/ogfx/gui/WidgetManager.cpp | 22 -- src/ogfx/gui/WidgetManager.hpp | 3 - src/ogfx/gui/Widgets.hpp | 1 + src/ogfx/gui/Window.cpp | 26 +- src/ogfx/gui/widgets/Text.cpp | 418 ++++++++++++++++++++++++- src/ogfx/gui/widgets/Text.hpp | 70 ++++- src/ogfx/utils/Keycodes.hpp | 538 ++++++++++++++++---------------- src/ostd/string/TextBuffer.cpp | 51 +++ src/ostd/string/TextBuffer.hpp | 2 + src/test/GraphicsWindowTest.cpp | 31 +- src/test/GuiTest.cpp | 2 + 17 files changed, 900 insertions(+), 351 deletions(-) diff --git a/build b/build index 09cb46b..34c9fae 100755 --- a/build +++ b/build @@ -43,5 +43,22 @@ else cd bin ./ostd_test 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 diff --git a/extra/DefaultThemeDark.oss b/extra/DefaultThemeDark.oss index 3d1050c..bb1ee80 100644 --- a/extra/DefaultThemeDark.oss +++ b/extra/DefaultThemeDark.oss @@ -388,3 +388,14 @@ macro set_common_values(_borderColor = $borderColor, _fontSize = 18, _showBorder set_focus_border($blueAccent) } % ====================== + + + +% ====== TextEdit ====== +(textEdit) { + set_common_values() + set_focus_border($blueAccent) + backgroundColor = $darkColor + textColor = $textColor +} +% ====================== diff --git a/other/TODO.txt b/other/TODO.txt index 23c7840..a57caa1 100644 --- a/other/TODO.txt +++ b/other/TODO.txt @@ -29,6 +29,9 @@ ***FIX: KeyPress crash on TreeView ***FIX: Animated icons not working in TreeView ***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 FIX: Window getting exponentially bigger when Desktop scale 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 Redesign theme overrides to be more robust 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: diff --git a/other/build.nr b/other/build.nr index ab7d2ad..fc9b6b9 100644 --- a/other/build.nr +++ b/other/build.nr @@ -1 +1 @@ -2076 +2077 diff --git a/src/ogfx/gui/Events.hpp b/src/ogfx/gui/Events.hpp index ac7efa3..3f818bb 100644 --- a/src/ogfx/gui/Events.hpp +++ b/src/ogfx/gui/Events.hpp @@ -70,7 +70,7 @@ namespace ogfx { public: enum class eKeyEvent { Pressed = 0, Released, Text }; 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"); validate(); @@ -78,7 +78,7 @@ namespace ogfx public: i32 keyCode; - char text; + String text; eKeyEvent eventType; WindowCore& parentWindow; }; diff --git a/src/ogfx/gui/RawTextInput.cpp b/src/ogfx/gui/RawTextInput.cpp index 6e2b68b..5f09aa6 100644 --- a/src/ogfx/gui/RawTextInput.cpp +++ b/src/ogfx/gui/RawTextInput.cpp @@ -1,27 +1,27 @@ /* - OmniaFramework - A collection of useful functionality - Copyright (C) 2026 OmniaX-Dev + OmniaFramework - A collection of useful functionality + 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 - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + OmniaFramework is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. - OmniaFramework is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + OmniaFramework is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with OmniaFramework. If not, see . + You should have received a copy of the GNU General Public License + along with OmniaFramework. If not, see . */ #include "RawTextInput.hpp" #include "../gui/Window.hpp" #include "../../io/Logger.hpp" -#include "utils/Keycodes.hpp" +#include "../utils/Keycodes.hpp" namespace ogfx { @@ -55,9 +55,9 @@ namespace ogfx OX_ERROR("Invalid character filter in RawTextInput event listener."); 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); return; @@ -66,10 +66,10 @@ namespace ogfx String s2 = ""; if (parent.m_cursorPosition < parent.m_text.len()) 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_cursorPosition++; - parent.m_lastChar = data.text; + parent.m_lastChar = data.text[0]; parent.m_keyRepeatCounter.start(); if (parent.m_theme.cursorBlink) parent.m_theme.cursorBlinkCounter.start(); diff --git a/src/ogfx/gui/WidgetManager.cpp b/src/ogfx/gui/WidgetManager.cpp index 1714736..b91761a 100644 --- a/src/ogfx/gui/WidgetManager.cpp +++ b/src/ogfx/gui/WidgetManager.cpp @@ -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) { for (auto& w : m_widgetList) diff --git a/src/ogfx/gui/WidgetManager.hpp b/src/ogfx/gui/WidgetManager.hpp index 377d5c4..658edbf 100644 --- a/src/ogfx/gui/WidgetManager.hpp +++ b/src/ogfx/gui/WidgetManager.hpp @@ -50,9 +50,6 @@ namespace ogfx void onMouseMoved(const Event& event); void onMouseScrolled(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 onWindowResized(const Event& event); void onWindowFocused(const Event& event); diff --git a/src/ogfx/gui/Widgets.hpp b/src/ogfx/gui/Widgets.hpp index 19e449c..9cfdc5f 100644 --- a/src/ogfx/gui/Widgets.hpp +++ b/src/ogfx/gui/Widgets.hpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace ogfx { diff --git a/src/ogfx/gui/Window.cpp b/src/ogfx/gui/Window.cpp index 38a1a89..5d37f95 100644 --- a/src/ogfx/gui/Window.cpp +++ b/src/ogfx/gui/Window.cpp @@ -617,21 +617,21 @@ namespace ogfx MouseEventData mmd = get_mouse_state(event); 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) { - 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); } 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); } - 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); } @@ -1002,7 +1002,9 @@ namespace ogfx evt.__original_signal_id = ostd::BuiltinSignals::KeyPressed; m_toolbar.__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) { @@ -1011,7 +1013,9 @@ namespace ogfx m_focusManager.onKeyReleased(evt); m_toolbar.__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) close(); } @@ -1021,7 +1025,9 @@ namespace ogfx evt.__original_signal_id = ostd::BuiltinSignals::TextEntered; m_toolbar.__onTextEntered(evt); m_statusbar.__onTextEntered(evt); - m_rootWidget.__onTextEntered(evt); + auto focused = m_focusManager.getFocused(); + if (focused) + focused->__onTextEntered(evt); } onSignal(signal); } diff --git a/src/ogfx/gui/widgets/Text.cpp b/src/ogfx/gui/widgets/Text.cpp index b93f7f0..c12b1ad 100644 --- a/src/ogfx/gui/widgets/Text.cpp +++ b/src/ogfx/gui/widgets/Text.cpp @@ -19,11 +19,427 @@ */ #include "Text.hpp" +#include "../../utils/Keycodes.hpp" +#include "../../render/BasicRenderer.hpp" -namespace ostd +namespace ogfx { 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 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((distLo <= distHi) ? bs[lo] : bs[hi]); + } } } diff --git a/src/ogfx/gui/widgets/Text.hpp b/src/ogfx/gui/widgets/Text.hpp index 6043cc9..dc39c08 100644 --- a/src/ogfx/gui/widgets/Text.hpp +++ b/src/ogfx/gui/widgets/Text.hpp @@ -20,11 +20,77 @@ #pragma once -#include "Widget.hpp" +#include +#include +#include -namespace ostd +namespace ogfx { 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 byteOffsets; // size N+1: byte offset before each codepoint, plus end + stdvec 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 + }; } } diff --git a/src/ogfx/utils/Keycodes.hpp b/src/ogfx/utils/Keycodes.hpp index cf678bd..7e25d56 100644 --- a/src/ogfx/utils/Keycodes.hpp +++ b/src/ogfx/utils/Keycodes.hpp @@ -1,21 +1,21 @@ /* - OmniaFramework - A collection of useful functionality - Copyright (C) 2026 OmniaX-Dev + OmniaFramework - A collection of useful functionality + 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 - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + OmniaFramework is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. - OmniaFramework is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + OmniaFramework is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with OmniaFramework. If not, see . + You should have received a copy of the GNU General Public License + along with OmniaFramework. If not, see . */ #pragma once @@ -26,261 +26,261 @@ namespace ogfx { struct KeyCode { - inline static constexpr u32 Unknown { SDLK_UNKNOWN }; - inline static constexpr u32 Return { SDLK_RETURN }; - inline static constexpr u32 Escape { SDLK_ESCAPE }; - inline static constexpr u32 Backspace { SDLK_BACKSPACE }; - inline static constexpr u32 Tab { SDLK_TAB }; - inline static constexpr u32 Space { SDLK_SPACE }; - inline static constexpr u32 Exclaim { SDLK_EXCLAIM }; - inline static constexpr u32 Dblapostrophe { SDLK_DBLAPOSTROPHE }; - inline static constexpr u32 Hash { SDLK_HASH }; - inline static constexpr u32 Dollar { SDLK_DOLLAR }; - inline static constexpr u32 Percent { SDLK_PERCENT }; - inline static constexpr u32 Ampersand { SDLK_AMPERSAND }; - inline static constexpr u32 Apostrophe { SDLK_APOSTROPHE }; - inline static constexpr u32 Leftparen { SDLK_LEFTPAREN }; - inline static constexpr u32 Rightparen { SDLK_RIGHTPAREN }; - inline static constexpr u32 Asterisk { SDLK_ASTERISK }; - inline static constexpr u32 Plus { SDLK_PLUS }; - inline static constexpr u32 Comma { SDLK_COMMA }; - inline static constexpr u32 Minus { SDLK_MINUS }; - inline static constexpr u32 Period { SDLK_PERIOD }; - inline static constexpr u32 Slash { SDLK_SLASH }; - inline static constexpr u32 Num0 { SDLK_0 }; - inline static constexpr u32 Num1 { SDLK_1 }; - inline static constexpr u32 Num2 { SDLK_2 }; - inline static constexpr u32 Num3 { SDLK_3 }; - inline static constexpr u32 Num4 { SDLK_4 }; - inline static constexpr u32 Num5 { SDLK_5 }; - inline static constexpr u32 Num6 { SDLK_6 }; - inline static constexpr u32 Num7 { SDLK_7 }; - inline static constexpr u32 Num8 { SDLK_8 }; - inline static constexpr u32 Num9 { SDLK_9 }; - inline static constexpr u32 Colon { SDLK_COLON }; - inline static constexpr u32 Semicolon { SDLK_SEMICOLON }; - inline static constexpr u32 Less { SDLK_LESS }; - inline static constexpr u32 Equals { SDLK_EQUALS }; - inline static constexpr u32 Greater { SDLK_GREATER }; - inline static constexpr u32 Question { SDLK_QUESTION }; - inline static constexpr u32 At { SDLK_AT }; - inline static constexpr u32 LeftBracket { SDLK_LEFTBRACKET }; - inline static constexpr u32 Backslash { SDLK_BACKSLASH }; - inline static constexpr u32 RightBracket { SDLK_RIGHTBRACKET }; - inline static constexpr u32 Caret { SDLK_CARET }; - inline static constexpr u32 Underscore { SDLK_UNDERSCORE }; - inline static constexpr u32 Grave { SDLK_GRAVE }; - inline static constexpr u32 A { SDLK_A }; - inline static constexpr u32 B { SDLK_B }; - inline static constexpr u32 C { SDLK_C }; - inline static constexpr u32 D { SDLK_D }; - inline static constexpr u32 E { SDLK_E }; - inline static constexpr u32 F { SDLK_F }; - inline static constexpr u32 G { SDLK_G }; - inline static constexpr u32 H { SDLK_H }; - inline static constexpr u32 I { SDLK_I }; - inline static constexpr u32 J { SDLK_J }; - inline static constexpr u32 K { SDLK_K }; - inline static constexpr u32 L { SDLK_L }; - inline static constexpr u32 M { SDLK_M }; - inline static constexpr u32 N { SDLK_N }; - inline static constexpr u32 O { SDLK_O }; - inline static constexpr u32 P { SDLK_P }; - inline static constexpr u32 Q { SDLK_Q }; - inline static constexpr u32 R { SDLK_R }; - inline static constexpr u32 S { SDLK_S }; - inline static constexpr u32 T { SDLK_T }; - inline static constexpr u32 U { SDLK_U }; - inline static constexpr u32 V { SDLK_V }; - inline static constexpr u32 W { SDLK_W }; - inline static constexpr u32 X { SDLK_X }; - inline static constexpr u32 Y { SDLK_Y }; - inline static constexpr u32 Z { SDLK_Z }; - inline static constexpr u32 Leftbrace { SDLK_LEFTBRACE }; - inline static constexpr u32 Pipe { SDLK_PIPE }; - inline static constexpr u32 Rightbrace { SDLK_RIGHTBRACE }; - inline static constexpr u32 Tilde { SDLK_TILDE }; - inline static constexpr u32 Delete { SDLK_DELETE }; - inline static constexpr u32 Plusminus { SDLK_PLUSMINUS }; - inline static constexpr u32 Capslock { SDLK_CAPSLOCK }; - inline static constexpr u32 F1 { SDLK_F1 }; - inline static constexpr u32 F2 { SDLK_F2 }; - inline static constexpr u32 F3 { SDLK_F3 }; - inline static constexpr u32 F4 { SDLK_F4 }; - inline static constexpr u32 F5 { SDLK_F5 }; - inline static constexpr u32 F6 { SDLK_F6 }; - inline static constexpr u32 F7 { SDLK_F7 }; - inline static constexpr u32 F8 { SDLK_F8 }; - inline static constexpr u32 F9 { SDLK_F9 }; - inline static constexpr u32 F10 { SDLK_F10 }; - inline static constexpr u32 F11 { SDLK_F11 }; - inline static constexpr u32 F12 { SDLK_F12 }; - inline static constexpr u32 Printscreen { SDLK_PRINTSCREEN }; - inline static constexpr u32 Scrolllock { SDLK_SCROLLLOCK }; - inline static constexpr u32 Pause { SDLK_PAUSE }; - inline static constexpr u32 Insert { SDLK_INSERT }; - inline static constexpr u32 Home { SDLK_HOME }; - inline static constexpr u32 Pageup { SDLK_PAGEUP }; - inline static constexpr u32 End { SDLK_END }; - inline static constexpr u32 Pagedown { SDLK_PAGEDOWN }; - inline static constexpr u32 Right { SDLK_RIGHT }; - inline static constexpr u32 Left { SDLK_LEFT }; - inline static constexpr u32 Down { SDLK_DOWN }; - inline static constexpr u32 Up { SDLK_UP }; - inline static constexpr u32 Numlockclear { SDLK_NUMLOCKCLEAR }; - inline static constexpr u32 KpDivide { SDLK_KP_DIVIDE }; - inline static constexpr u32 KpMultiply { SDLK_KP_MULTIPLY }; - inline static constexpr u32 KpMinus { SDLK_KP_MINUS }; - inline static constexpr u32 KpPlus { SDLK_KP_PLUS }; - inline static constexpr u32 KpEnter { SDLK_KP_ENTER }; - inline static constexpr u32 Kp1 { SDLK_KP_1 }; - inline static constexpr u32 Kp2 { SDLK_KP_2 }; - inline static constexpr u32 Kp3 { SDLK_KP_3 }; - inline static constexpr u32 Kp4 { SDLK_KP_4 }; - inline static constexpr u32 Kp5 { SDLK_KP_5 }; - inline static constexpr u32 Kp6 { SDLK_KP_6 }; - inline static constexpr u32 Kp7 { SDLK_KP_7 }; - inline static constexpr u32 Kp8 { SDLK_KP_8 }; - inline static constexpr u32 Kp9 { SDLK_KP_9 }; - inline static constexpr u32 Kp0 { SDLK_KP_0 }; - inline static constexpr u32 KpPeriod { SDLK_KP_PERIOD }; - inline static constexpr u32 Application { SDLK_APPLICATION }; - inline static constexpr u32 Power { SDLK_POWER }; - inline static constexpr u32 KpEquals { SDLK_KP_EQUALS }; - inline static constexpr u32 F13 { SDLK_F13 }; - inline static constexpr u32 F14 { SDLK_F14 }; - inline static constexpr u32 F15 { SDLK_F15 }; - inline static constexpr u32 F16 { SDLK_F16 }; - inline static constexpr u32 F17 { SDLK_F17 }; - inline static constexpr u32 F18 { SDLK_F18 }; - inline static constexpr u32 F19 { SDLK_F19 }; - inline static constexpr u32 F20 { SDLK_F20 }; - inline static constexpr u32 F21 { SDLK_F21 }; - inline static constexpr u32 F22 { SDLK_F22 }; - inline static constexpr u32 F23 { SDLK_F23 }; - inline static constexpr u32 F24 { SDLK_F24 }; - inline static constexpr u32 Execute { SDLK_EXECUTE }; - inline static constexpr u32 Help { SDLK_HELP }; - inline static constexpr u32 Menu { SDLK_MENU }; - inline static constexpr u32 Select { SDLK_SELECT }; - inline static constexpr u32 Stop { SDLK_STOP }; - inline static constexpr u32 Again { SDLK_AGAIN }; - inline static constexpr u32 Undo { SDLK_UNDO }; - inline static constexpr u32 Cut { SDLK_CUT }; - inline static constexpr u32 Copy { SDLK_COPY }; - inline static constexpr u32 Paste { SDLK_PASTE }; - inline static constexpr u32 Find { SDLK_FIND }; - inline static constexpr u32 Mute { SDLK_MUTE }; - inline static constexpr u32 Volumeup { SDLK_VOLUMEUP }; - inline static constexpr u32 Volumedown { SDLK_VOLUMEDOWN }; - inline static constexpr u32 KpComma { SDLK_KP_COMMA }; - inline static constexpr u32 KpEqualsas400 { SDLK_KP_EQUALSAS400 }; - inline static constexpr u32 Alterase { SDLK_ALTERASE }; - inline static constexpr u32 Sysreq { SDLK_SYSREQ }; - inline static constexpr u32 Cancel { SDLK_CANCEL }; - inline static constexpr u32 Clear { SDLK_CLEAR }; - inline static constexpr u32 Prior { SDLK_PRIOR }; - inline static constexpr u32 Return2 { SDLK_RETURN2 }; - inline static constexpr u32 Separator { SDLK_SEPARATOR }; - inline static constexpr u32 Out { SDLK_OUT }; - inline static constexpr u32 Oper { SDLK_OPER }; - inline static constexpr u32 Clearagain { SDLK_CLEARAGAIN }; - inline static constexpr u32 Crsel { SDLK_CRSEL }; - inline static constexpr u32 Exsel { SDLK_EXSEL }; - inline static constexpr u32 Kp00 { SDLK_KP_00 }; - inline static constexpr u32 Kp000 { SDLK_KP_000 }; - inline static constexpr u32 Thousandsseparator { SDLK_THOUSANDSSEPARATOR }; - inline static constexpr u32 Decimalseparator { SDLK_DECIMALSEPARATOR }; - inline static constexpr u32 Currencyunit { SDLK_CURRENCYUNIT }; - inline static constexpr u32 Currencysubunit { SDLK_CURRENCYSUBUNIT }; - inline static constexpr u32 KpLeftparen { SDLK_KP_LEFTPAREN }; - inline static constexpr u32 KpRightparen { SDLK_KP_RIGHTPAREN }; - inline static constexpr u32 KpLeftbrace { SDLK_KP_LEFTBRACE }; - inline static constexpr u32 KpRightbrace { SDLK_KP_RIGHTBRACE }; - inline static constexpr u32 KpTab { SDLK_KP_TAB }; - inline static constexpr u32 KpBackspace { SDLK_KP_BACKSPACE }; - inline static constexpr u32 KpA { SDLK_KP_A }; - inline static constexpr u32 KpB { SDLK_KP_B }; - inline static constexpr u32 KpC { SDLK_KP_C }; - inline static constexpr u32 KpD { SDLK_KP_D }; - inline static constexpr u32 KpE { SDLK_KP_E }; - inline static constexpr u32 KpF { SDLK_KP_F }; - inline static constexpr u32 KpXor { SDLK_KP_XOR }; - inline static constexpr u32 KpPower { SDLK_KP_POWER }; - inline static constexpr u32 KpPercent { SDLK_KP_PERCENT }; - inline static constexpr u32 KpLess { SDLK_KP_LESS }; - inline static constexpr u32 KpGreater { SDLK_KP_GREATER }; - inline static constexpr u32 KpAmpersand { SDLK_KP_AMPERSAND }; - inline static constexpr u32 KpDblampersand { SDLK_KP_DBLAMPERSAND }; - inline static constexpr u32 KpVerticalbar { SDLK_KP_VERTICALBAR }; - inline static constexpr u32 KpDblverticalbar { SDLK_KP_DBLVERTICALBAR }; - inline static constexpr u32 KpColon { SDLK_KP_COLON }; - inline static constexpr u32 KpHash { SDLK_KP_HASH }; - inline static constexpr u32 KpSpace { SDLK_KP_SPACE }; - inline static constexpr u32 KpAt { SDLK_KP_AT }; - inline static constexpr u32 KpExclam { SDLK_KP_EXCLAM }; - inline static constexpr u32 KpMemstore { SDLK_KP_MEMSTORE }; - inline static constexpr u32 KpMemrecall { SDLK_KP_MEMRECALL }; - inline static constexpr u32 KpMemclear { SDLK_KP_MEMCLEAR }; - inline static constexpr u32 KpMemadd { SDLK_KP_MEMADD }; - inline static constexpr u32 KpMemsubtract { SDLK_KP_MEMSUBTRACT }; - inline static constexpr u32 KpMemmultiply { SDLK_KP_MEMMULTIPLY }; - inline static constexpr u32 KpMemdivide { SDLK_KP_MEMDIVIDE }; - inline static constexpr u32 KpPlusminus { SDLK_KP_PLUSMINUS }; - inline static constexpr u32 KpClear { SDLK_KP_CLEAR }; - inline static constexpr u32 KpClearentry { SDLK_KP_CLEARENTRY }; - inline static constexpr u32 KpBinary { SDLK_KP_BINARY }; - inline static constexpr u32 KpOctal { SDLK_KP_OCTAL }; - inline static constexpr u32 KpDecimal { SDLK_KP_DECIMAL }; - inline static constexpr u32 KpHexadecimal { SDLK_KP_HEXADECIMAL }; - inline static constexpr u32 Lctrl { SDLK_LCTRL }; - inline static constexpr u32 Lshift { SDLK_LSHIFT }; - inline static constexpr u32 Lalt { SDLK_LALT }; - inline static constexpr u32 Lgui { SDLK_LGUI }; - inline static constexpr u32 Rctrl { SDLK_RCTRL }; - inline static constexpr u32 Rshift { SDLK_RSHIFT }; - inline static constexpr u32 Ralt { SDLK_RALT }; - inline static constexpr u32 Rgui { SDLK_RGUI }; - inline static constexpr u32 Mode { SDLK_MODE }; - inline static constexpr u32 Sleep { SDLK_SLEEP }; - inline static constexpr u32 Wake { SDLK_WAKE }; - inline static constexpr u32 ChannelIncrement { SDLK_CHANNEL_INCREMENT }; - inline static constexpr u32 ChannelDecrement { SDLK_CHANNEL_DECREMENT }; - inline static constexpr u32 MediaPlay { SDLK_MEDIA_PLAY }; - inline static constexpr u32 MediaPause { SDLK_MEDIA_PAUSE }; - inline static constexpr u32 MediaRecord { SDLK_MEDIA_RECORD }; - inline static constexpr u32 MediaFastForward { SDLK_MEDIA_FAST_FORWARD }; - inline static constexpr u32 MediaRewind { SDLK_MEDIA_REWIND }; - inline static constexpr u32 MediaNextTrack { SDLK_MEDIA_NEXT_TRACK }; - inline static constexpr u32 MediaPreviousTrack { SDLK_MEDIA_PREVIOUS_TRACK }; - inline static constexpr u32 MediaStop { SDLK_MEDIA_STOP }; - inline static constexpr u32 MediaEject { SDLK_MEDIA_EJECT }; - inline static constexpr u32 MediaPlayPause { SDLK_MEDIA_PLAY_PAUSE }; - inline static constexpr u32 MediaSelect { SDLK_MEDIA_SELECT }; - inline static constexpr u32 AcNew { SDLK_AC_NEW }; - inline static constexpr u32 AcOpen { SDLK_AC_OPEN }; - inline static constexpr u32 AcClose { SDLK_AC_CLOSE }; - inline static constexpr u32 AcExit { SDLK_AC_EXIT }; - inline static constexpr u32 AcSave { SDLK_AC_SAVE }; - inline static constexpr u32 AcPrint { SDLK_AC_PRINT }; - inline static constexpr u32 AcProperties { SDLK_AC_PROPERTIES }; - inline static constexpr u32 AcSearch { SDLK_AC_SEARCH }; - inline static constexpr u32 AcHome { SDLK_AC_HOME }; - inline static constexpr u32 AcBack { SDLK_AC_BACK }; - inline static constexpr u32 AcForward { SDLK_AC_FORWARD }; - inline static constexpr u32 AcStop { SDLK_AC_STOP }; - inline static constexpr u32 AcRefresh { SDLK_AC_REFRESH }; - inline static constexpr u32 AcBookmarks { SDLK_AC_BOOKMARKS }; - inline static constexpr u32 Softleft { SDLK_SOFTLEFT }; - inline static constexpr u32 Softright { SDLK_SOFTRIGHT }; - inline static constexpr u32 Call { SDLK_CALL }; - inline static constexpr u32 Endcall { SDLK_ENDCALL }; - inline static constexpr u32 LeftTab { SDLK_LEFT_TAB }; - inline static constexpr u32 Level5Shift { SDLK_LEVEL5_SHIFT }; - inline static constexpr u32 MultiKeyCompose { SDLK_MULTI_KEY_COMPOSE }; - inline static constexpr u32 Lmeta { SDLK_LMETA }; - inline static constexpr u32 Rmeta { SDLK_RMETA }; - inline static constexpr u32 Lhyper { SDLK_LHYPER }; - inline static constexpr u32 Rhyper { SDLK_RHYPER }; + inline static constexpr u32 Unknown { SDLK_UNKNOWN }; + inline static constexpr u32 Return { SDLK_RETURN }; + inline static constexpr u32 Escape { SDLK_ESCAPE }; + inline static constexpr u32 Backspace { SDLK_BACKSPACE }; + inline static constexpr u32 Tab { SDLK_TAB }; + inline static constexpr u32 Space { SDLK_SPACE }; + inline static constexpr u32 Exclaim { SDLK_EXCLAIM }; + inline static constexpr u32 Dblapostrophe { SDLK_DBLAPOSTROPHE }; + inline static constexpr u32 Hash { SDLK_HASH }; + inline static constexpr u32 Dollar { SDLK_DOLLAR }; + inline static constexpr u32 Percent { SDLK_PERCENT }; + inline static constexpr u32 Ampersand { SDLK_AMPERSAND }; + inline static constexpr u32 Apostrophe { SDLK_APOSTROPHE }; + inline static constexpr u32 Leftparen { SDLK_LEFTPAREN }; + inline static constexpr u32 Rightparen { SDLK_RIGHTPAREN }; + inline static constexpr u32 Asterisk { SDLK_ASTERISK }; + inline static constexpr u32 Plus { SDLK_PLUS }; + inline static constexpr u32 Comma { SDLK_COMMA }; + inline static constexpr u32 Minus { SDLK_MINUS }; + inline static constexpr u32 Period { SDLK_PERIOD }; + inline static constexpr u32 Slash { SDLK_SLASH }; + inline static constexpr u32 Num0 { SDLK_0 }; + inline static constexpr u32 Num1 { SDLK_1 }; + inline static constexpr u32 Num2 { SDLK_2 }; + inline static constexpr u32 Num3 { SDLK_3 }; + inline static constexpr u32 Num4 { SDLK_4 }; + inline static constexpr u32 Num5 { SDLK_5 }; + inline static constexpr u32 Num6 { SDLK_6 }; + inline static constexpr u32 Num7 { SDLK_7 }; + inline static constexpr u32 Num8 { SDLK_8 }; + inline static constexpr u32 Num9 { SDLK_9 }; + inline static constexpr u32 Colon { SDLK_COLON }; + inline static constexpr u32 Semicolon { SDLK_SEMICOLON }; + inline static constexpr u32 Less { SDLK_LESS }; + inline static constexpr u32 Equals { SDLK_EQUALS }; + inline static constexpr u32 Greater { SDLK_GREATER }; + inline static constexpr u32 Question { SDLK_QUESTION }; + inline static constexpr u32 At { SDLK_AT }; + inline static constexpr u32 LeftBracket { SDLK_LEFTBRACKET }; + inline static constexpr u32 Backslash { SDLK_BACKSLASH }; + inline static constexpr u32 RightBracket { SDLK_RIGHTBRACKET }; + inline static constexpr u32 Caret { SDLK_CARET }; + inline static constexpr u32 Underscore { SDLK_UNDERSCORE }; + inline static constexpr u32 Grave { SDLK_GRAVE }; + inline static constexpr u32 A { SDLK_A }; + inline static constexpr u32 B { SDLK_B }; + inline static constexpr u32 C { SDLK_C }; + inline static constexpr u32 D { SDLK_D }; + inline static constexpr u32 E { SDLK_E }; + inline static constexpr u32 F { SDLK_F }; + inline static constexpr u32 G { SDLK_G }; + inline static constexpr u32 H { SDLK_H }; + inline static constexpr u32 I { SDLK_I }; + inline static constexpr u32 J { SDLK_J }; + inline static constexpr u32 K { SDLK_K }; + inline static constexpr u32 L { SDLK_L }; + inline static constexpr u32 M { SDLK_M }; + inline static constexpr u32 N { SDLK_N }; + inline static constexpr u32 O { SDLK_O }; + inline static constexpr u32 P { SDLK_P }; + inline static constexpr u32 Q { SDLK_Q }; + inline static constexpr u32 R { SDLK_R }; + inline static constexpr u32 S { SDLK_S }; + inline static constexpr u32 T { SDLK_T }; + inline static constexpr u32 U { SDLK_U }; + inline static constexpr u32 V { SDLK_V }; + inline static constexpr u32 W { SDLK_W }; + inline static constexpr u32 X { SDLK_X }; + inline static constexpr u32 Y { SDLK_Y }; + inline static constexpr u32 Z { SDLK_Z }; + inline static constexpr u32 Leftbrace { SDLK_LEFTBRACE }; + inline static constexpr u32 Pipe { SDLK_PIPE }; + inline static constexpr u32 Rightbrace { SDLK_RIGHTBRACE }; + inline static constexpr u32 Tilde { SDLK_TILDE }; + inline static constexpr u32 Delete { SDLK_DELETE }; + inline static constexpr u32 Plusminus { SDLK_PLUSMINUS }; + inline static constexpr u32 Capslock { SDLK_CAPSLOCK }; + inline static constexpr u32 F1 { SDLK_F1 }; + inline static constexpr u32 F2 { SDLK_F2 }; + inline static constexpr u32 F3 { SDLK_F3 }; + inline static constexpr u32 F4 { SDLK_F4 }; + inline static constexpr u32 F5 { SDLK_F5 }; + inline static constexpr u32 F6 { SDLK_F6 }; + inline static constexpr u32 F7 { SDLK_F7 }; + inline static constexpr u32 F8 { SDLK_F8 }; + inline static constexpr u32 F9 { SDLK_F9 }; + inline static constexpr u32 F10 { SDLK_F10 }; + inline static constexpr u32 F11 { SDLK_F11 }; + inline static constexpr u32 F12 { SDLK_F12 }; + inline static constexpr u32 Printscreen { SDLK_PRINTSCREEN }; + inline static constexpr u32 Scrolllock { SDLK_SCROLLLOCK }; + inline static constexpr u32 Pause { SDLK_PAUSE }; + inline static constexpr u32 Insert { SDLK_INSERT }; + inline static constexpr u32 Home { SDLK_HOME }; + inline static constexpr u32 Pageup { SDLK_PAGEUP }; + inline static constexpr u32 End { SDLK_END }; + inline static constexpr u32 Pagedown { SDLK_PAGEDOWN }; + inline static constexpr u32 Right { SDLK_RIGHT }; + inline static constexpr u32 Left { SDLK_LEFT }; + inline static constexpr u32 Down { SDLK_DOWN }; + inline static constexpr u32 Up { SDLK_UP }; + inline static constexpr u32 Numlockclear { SDLK_NUMLOCKCLEAR }; + inline static constexpr u32 KpDivide { SDLK_KP_DIVIDE }; + inline static constexpr u32 KpMultiply { SDLK_KP_MULTIPLY }; + inline static constexpr u32 KpMinus { SDLK_KP_MINUS }; + inline static constexpr u32 KpPlus { SDLK_KP_PLUS }; + inline static constexpr u32 KpEnter { SDLK_KP_ENTER }; + inline static constexpr u32 Kp1 { SDLK_KP_1 }; + inline static constexpr u32 Kp2 { SDLK_KP_2 }; + inline static constexpr u32 Kp3 { SDLK_KP_3 }; + inline static constexpr u32 Kp4 { SDLK_KP_4 }; + inline static constexpr u32 Kp5 { SDLK_KP_5 }; + inline static constexpr u32 Kp6 { SDLK_KP_6 }; + inline static constexpr u32 Kp7 { SDLK_KP_7 }; + inline static constexpr u32 Kp8 { SDLK_KP_8 }; + inline static constexpr u32 Kp9 { SDLK_KP_9 }; + inline static constexpr u32 Kp0 { SDLK_KP_0 }; + inline static constexpr u32 KpPeriod { SDLK_KP_PERIOD }; + inline static constexpr u32 Application { SDLK_APPLICATION }; + inline static constexpr u32 Power { SDLK_POWER }; + inline static constexpr u32 KpEquals { SDLK_KP_EQUALS }; + inline static constexpr u32 F13 { SDLK_F13 }; + inline static constexpr u32 F14 { SDLK_F14 }; + inline static constexpr u32 F15 { SDLK_F15 }; + inline static constexpr u32 F16 { SDLK_F16 }; + inline static constexpr u32 F17 { SDLK_F17 }; + inline static constexpr u32 F18 { SDLK_F18 }; + inline static constexpr u32 F19 { SDLK_F19 }; + inline static constexpr u32 F20 { SDLK_F20 }; + inline static constexpr u32 F21 { SDLK_F21 }; + inline static constexpr u32 F22 { SDLK_F22 }; + inline static constexpr u32 F23 { SDLK_F23 }; + inline static constexpr u32 F24 { SDLK_F24 }; + inline static constexpr u32 Execute { SDLK_EXECUTE }; + inline static constexpr u32 Help { SDLK_HELP }; + inline static constexpr u32 Menu { SDLK_MENU }; + inline static constexpr u32 Select { SDLK_SELECT }; + inline static constexpr u32 Stop { SDLK_STOP }; + inline static constexpr u32 Again { SDLK_AGAIN }; + inline static constexpr u32 Undo { SDLK_UNDO }; + inline static constexpr u32 Cut { SDLK_CUT }; + inline static constexpr u32 Copy { SDLK_COPY }; + inline static constexpr u32 Paste { SDLK_PASTE }; + inline static constexpr u32 Find { SDLK_FIND }; + inline static constexpr u32 Mute { SDLK_MUTE }; + inline static constexpr u32 Volumeup { SDLK_VOLUMEUP }; + inline static constexpr u32 Volumedown { SDLK_VOLUMEDOWN }; + inline static constexpr u32 KpComma { SDLK_KP_COMMA }; + inline static constexpr u32 KpEqualsas400 { SDLK_KP_EQUALSAS400 }; + inline static constexpr u32 Alterase { SDLK_ALTERASE }; + inline static constexpr u32 Sysreq { SDLK_SYSREQ }; + inline static constexpr u32 Cancel { SDLK_CANCEL }; + inline static constexpr u32 Clear { SDLK_CLEAR }; + inline static constexpr u32 Prior { SDLK_PRIOR }; + inline static constexpr u32 Return2 { SDLK_RETURN2 }; + inline static constexpr u32 Separator { SDLK_SEPARATOR }; + inline static constexpr u32 Out { SDLK_OUT }; + inline static constexpr u32 Oper { SDLK_OPER }; + inline static constexpr u32 Clearagain { SDLK_CLEARAGAIN }; + inline static constexpr u32 Crsel { SDLK_CRSEL }; + inline static constexpr u32 Exsel { SDLK_EXSEL }; + inline static constexpr u32 Kp00 { SDLK_KP_00 }; + inline static constexpr u32 Kp000 { SDLK_KP_000 }; + inline static constexpr u32 Thousandsseparator { SDLK_THOUSANDSSEPARATOR }; + inline static constexpr u32 Decimalseparator { SDLK_DECIMALSEPARATOR }; + inline static constexpr u32 Currencyunit { SDLK_CURRENCYUNIT }; + inline static constexpr u32 Currencysubunit { SDLK_CURRENCYSUBUNIT }; + inline static constexpr u32 KpLeftparen { SDLK_KP_LEFTPAREN }; + inline static constexpr u32 KpRightparen { SDLK_KP_RIGHTPAREN }; + inline static constexpr u32 KpLeftbrace { SDLK_KP_LEFTBRACE }; + inline static constexpr u32 KpRightbrace { SDLK_KP_RIGHTBRACE }; + inline static constexpr u32 KpTab { SDLK_KP_TAB }; + inline static constexpr u32 KpBackspace { SDLK_KP_BACKSPACE }; + inline static constexpr u32 KpA { SDLK_KP_A }; + inline static constexpr u32 KpB { SDLK_KP_B }; + inline static constexpr u32 KpC { SDLK_KP_C }; + inline static constexpr u32 KpD { SDLK_KP_D }; + inline static constexpr u32 KpE { SDLK_KP_E }; + inline static constexpr u32 KpF { SDLK_KP_F }; + inline static constexpr u32 KpXor { SDLK_KP_XOR }; + inline static constexpr u32 KpPower { SDLK_KP_POWER }; + inline static constexpr u32 KpPercent { SDLK_KP_PERCENT }; + inline static constexpr u32 KpLess { SDLK_KP_LESS }; + inline static constexpr u32 KpGreater { SDLK_KP_GREATER }; + inline static constexpr u32 KpAmpersand { SDLK_KP_AMPERSAND }; + inline static constexpr u32 KpDblampersand { SDLK_KP_DBLAMPERSAND }; + inline static constexpr u32 KpVerticalbar { SDLK_KP_VERTICALBAR }; + inline static constexpr u32 KpDblverticalbar { SDLK_KP_DBLVERTICALBAR }; + inline static constexpr u32 KpColon { SDLK_KP_COLON }; + inline static constexpr u32 KpHash { SDLK_KP_HASH }; + inline static constexpr u32 KpSpace { SDLK_KP_SPACE }; + inline static constexpr u32 KpAt { SDLK_KP_AT }; + inline static constexpr u32 KpExclam { SDLK_KP_EXCLAM }; + inline static constexpr u32 KpMemstore { SDLK_KP_MEMSTORE }; + inline static constexpr u32 KpMemrecall { SDLK_KP_MEMRECALL }; + inline static constexpr u32 KpMemclear { SDLK_KP_MEMCLEAR }; + inline static constexpr u32 KpMemadd { SDLK_KP_MEMADD }; + inline static constexpr u32 KpMemsubtract { SDLK_KP_MEMSUBTRACT }; + inline static constexpr u32 KpMemmultiply { SDLK_KP_MEMMULTIPLY }; + inline static constexpr u32 KpMemdivide { SDLK_KP_MEMDIVIDE }; + inline static constexpr u32 KpPlusminus { SDLK_KP_PLUSMINUS }; + inline static constexpr u32 KpClear { SDLK_KP_CLEAR }; + inline static constexpr u32 KpClearentry { SDLK_KP_CLEARENTRY }; + inline static constexpr u32 KpBinary { SDLK_KP_BINARY }; + inline static constexpr u32 KpOctal { SDLK_KP_OCTAL }; + inline static constexpr u32 KpDecimal { SDLK_KP_DECIMAL }; + inline static constexpr u32 KpHexadecimal { SDLK_KP_HEXADECIMAL }; + inline static constexpr u32 Lctrl { SDLK_LCTRL }; + inline static constexpr u32 Lshift { SDLK_LSHIFT }; + inline static constexpr u32 Lalt { SDLK_LALT }; + inline static constexpr u32 Lgui { SDLK_LGUI }; + inline static constexpr u32 Rctrl { SDLK_RCTRL }; + inline static constexpr u32 Rshift { SDLK_RSHIFT }; + inline static constexpr u32 Ralt { SDLK_RALT }; + inline static constexpr u32 Rgui { SDLK_RGUI }; + inline static constexpr u32 Mode { SDLK_MODE }; + inline static constexpr u32 Sleep { SDLK_SLEEP }; + inline static constexpr u32 Wake { SDLK_WAKE }; + inline static constexpr u32 ChannelIncrement { SDLK_CHANNEL_INCREMENT }; + inline static constexpr u32 ChannelDecrement { SDLK_CHANNEL_DECREMENT }; + inline static constexpr u32 MediaPlay { SDLK_MEDIA_PLAY }; + inline static constexpr u32 MediaPause { SDLK_MEDIA_PAUSE }; + inline static constexpr u32 MediaRecord { SDLK_MEDIA_RECORD }; + inline static constexpr u32 MediaFastForward { SDLK_MEDIA_FAST_FORWARD }; + inline static constexpr u32 MediaRewind { SDLK_MEDIA_REWIND }; + inline static constexpr u32 MediaNextTrack { SDLK_MEDIA_NEXT_TRACK }; + inline static constexpr u32 MediaPreviousTrack { SDLK_MEDIA_PREVIOUS_TRACK }; + inline static constexpr u32 MediaStop { SDLK_MEDIA_STOP }; + inline static constexpr u32 MediaEject { SDLK_MEDIA_EJECT }; + inline static constexpr u32 MediaPlayPause { SDLK_MEDIA_PLAY_PAUSE }; + inline static constexpr u32 MediaSelect { SDLK_MEDIA_SELECT }; + inline static constexpr u32 AcNew { SDLK_AC_NEW }; + inline static constexpr u32 AcOpen { SDLK_AC_OPEN }; + inline static constexpr u32 AcClose { SDLK_AC_CLOSE }; + inline static constexpr u32 AcExit { SDLK_AC_EXIT }; + inline static constexpr u32 AcSave { SDLK_AC_SAVE }; + inline static constexpr u32 AcPrint { SDLK_AC_PRINT }; + inline static constexpr u32 AcProperties { SDLK_AC_PROPERTIES }; + inline static constexpr u32 AcSearch { SDLK_AC_SEARCH }; + inline static constexpr u32 AcHome { SDLK_AC_HOME }; + inline static constexpr u32 AcBack { SDLK_AC_BACK }; + inline static constexpr u32 AcForward { SDLK_AC_FORWARD }; + inline static constexpr u32 AcStop { SDLK_AC_STOP }; + inline static constexpr u32 AcRefresh { SDLK_AC_REFRESH }; + inline static constexpr u32 AcBookmarks { SDLK_AC_BOOKMARKS }; + inline static constexpr u32 Softleft { SDLK_SOFTLEFT }; + inline static constexpr u32 Softright { SDLK_SOFTRIGHT }; + inline static constexpr u32 Call { SDLK_CALL }; + inline static constexpr u32 Endcall { SDLK_ENDCALL }; + inline static constexpr u32 LeftTab { SDLK_LEFT_TAB }; + inline static constexpr u32 Level5Shift { SDLK_LEVEL5_SHIFT }; + inline static constexpr u32 MultiKeyCompose { SDLK_MULTI_KEY_COMPOSE }; + inline static constexpr u32 Lmeta { SDLK_LMETA }; + inline static constexpr u32 Rmeta { SDLK_RMETA }; + inline static constexpr u32 Lhyper { SDLK_LHYPER }; + inline static constexpr u32 Rhyper { SDLK_RHYPER }; }; } diff --git a/src/ostd/string/TextBuffer.cpp b/src/ostd/string/TextBuffer.cpp index c043fba..0da5729 100644 --- a/src/ostd/string/TextBuffer.cpp +++ b/src/ostd/string/TextBuffer.cpp @@ -325,6 +325,13 @@ namespace ostd move_cursor_to(cast(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 @@ -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(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(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(s[end])) != cls) break; + end = String::utf8::next_codepoint_start(s, end); + } + + selectRange(start, end); + } + u32 TextBuffer::prev_word_boundary(u32 byte_offset) const { const cpp_string& s = m_buffer.cpp_str(); diff --git a/src/ostd/string/TextBuffer.hpp b/src/ostd/string/TextBuffer.hpp index 968b60c..b9f7d3f 100644 --- a/src/ostd/string/TextBuffer.hpp +++ b/src/ostd/string/TextBuffer.hpp @@ -94,6 +94,7 @@ namespace ostd String selectedText(void) const; void clearSelection(void); void selectAll(void); + void selectWordAt(u32 byte_offset); void selectRange(u32 start_byte, u32 end_byte); // ===================== Cursor movement ==================== @@ -108,6 +109,7 @@ namespace ostd void moveLineEnd(bool extend = false); void moveDocumentStart(bool extend = false); void moveDocumentEnd(bool extend = false); + void setCursorByteOffset(u32 byte_offset, bool extend); // ======================== Mutation ======================== // All mutators replace the current selection (if any) before diff --git a/src/test/GraphicsWindowTest.cpp b/src/test/GraphicsWindowTest.cpp index 03e6bbb..f5b68c6 100644 --- a/src/test/GraphicsWindowTest.cpp +++ b/src/test/GraphicsWindowTest.cpp @@ -1,21 +1,21 @@ /* - OmniaFramework - A collection of useful functionality - Copyright (C) 2026 OmniaX-Dev + OmniaFramework - A collection of useful functionality + 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 - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + OmniaFramework is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. - OmniaFramework is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + OmniaFramework is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with OmniaFramework. If not, see . + You should have received a copy of the GNU General Public License + along with OmniaFramework. If not, see . */ #include "utils/Keycodes.hpp" @@ -29,9 +29,8 @@ class Window : public ogfx::GraphicsWindow inline Window(void) { } inline void onInitialize(void) override { - connectSignal(ogfx::gui::RawTextInput::actionEventSignalID); m_gfx.init(*this); - } + } inline void handleSignal(ostd::Signal& signal) override { @@ -71,7 +70,7 @@ i32 main(i32 argc, char** argv) { Window window; window.initialize(800, 600, "OmniaFramework - Test Window"); - window.setClearColor({ 0, 2 , 15 }); + window.setClearColor({ 0, 2 , 15 }); while (window.isRunning()) { diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index 0b5edd6..d0e2a1b 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -229,6 +229,7 @@ class TestWindow : public Window t1.addWidget(m_list, { 30, 300 }); t1.addWidget(m_panel2, { 500, 100 }); t1.addWidget(m_combo, { 400, 600 }); + t1.addWidget(m_text, { 400, 700 }); t2.setLayout(BoxLayout::Orientation::Vertical); t2.getLayout()->setSpacing(16); @@ -390,6 +391,7 @@ class TestWindow : public Window Label m_cacheHitsLbl { *this }; Label m_cacheMissesLbl { *this }; ComboBox m_combo { *this }; + TextEdit m_text { *this }; enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted };