Files
imgui-luau/imstb_textedit.lua

1540 lines
58 KiB
Lua

--- Dear ImGui
-- This is a Lua port of original `imstb_textedit.h`
-- [DEAR IMGUI]
-- This is a slightly modified version of stb_textedit.h 1.14.
-- Those changes would need to be pushed into nothings/stb:
-- - Fix in stb_textedit_discard_redo (see https://github.com/nothings/stb/issues/321)
-- - Fix in stb_textedit_find_charpos to handle last line (see https://github.com/ocornut/imgui/issues/6000 + #6783)
-- - Added name to struct or it may be forward declared in our code.
-- - Added UTF-8 support (see https://github.com/nothings/stb/issues/188 + https://github.com/ocornut/imgui/pull/7925)
-- - Changed STB_TEXTEDIT_INSERTCHARS() to return inserted count (instead of 0/1 bool), allowing partial insertion.
-- Grep for [DEAR IMGUI] to find some changes.
-- - Also renamed macros used or defined outside of IMSTB_TEXTEDIT_IMPLEMENTATION block from STB_TEXTEDIT_* to IMSTB_TEXTEDIT_*
-- stb_textedit.h - v1.14 - public domain - Sean Barrett
-- Development of this library was sponsored by RAD Game Tools
--
-- This C header file implements the guts of a multi-line text-editing
-- widget; you implement display, word-wrapping, and low-level string
-- insertion/deletion, and stb_textedit will map user inputs into
-- insertions & deletions, plus updates to the cursor position,
-- selection state, and undo state.
--
-- It is intended for use in games and other systems that need to build
-- their own custom widgets and which do not have heavy text-editing
-- requirements (this library is not recommended for use for editing large
-- texts, as its performance does not scale and it has limited undo).
--
-- Non-trivial behaviors are modelled after Windows text controls.
--
--
-- LICENSE
--
-- See end of file for license information.
--
--
-- DEPENDENCIES
--
-- Uses the C runtime function 'memmove', which you can override
-- by defining IMSTB_TEXTEDIT_memmove before the implementation.
-- (In this port: ImStb.TEXTEDIT_memmove.)
--
--
-- VERSION HISTORY
--
-- !!!! (2025-10-23) changed STB_TEXTEDIT_INSERTCHARS() to return inserted count (instead of 0/1 bool), allowing partial insertion.
-- 1.14 (2021-07-11) page up/down, various fixes
-- 1.13 (2019-02-07) fix bug in undo size management
-- 1.12 (2018-01-29) user can change STB_TEXTEDIT_KEYTYPE, fix redo to avoid crash
-- 1.11 (2017-03-03) fix HOME on last line, dragging off single-line textfield
-- 1.10 (2016-10-25) suppress warnings about casting away const with -Wcast-qual
-- 1.9 (2016-08-27) customizable move-by-word
-- 1.8 (2016-04-02) better keyboard handling when mouse button is down
-- 1.7 (2015-09-13) change y range handling in case baseline is non-0
-- 1.6 (2015-04-15) allow STB_TEXTEDIT_memmove
-- 1.5 (2014-09-10) add support for secondary keys for OS X
-- 1.4 (2014-08-17) fix signed/unsigned warnings
-- 1.3 (2014-06-19) fix mouse clicking to round to nearest char boundary
-- 1.2 (2014-05-27) fix some RAD types that had crept into the new code
-- 1.1 (2013-12-15) move-by-word (requires STB_TEXTEDIT_IS_SPACE )
-- 1.0 (2012-07-26) improve documentation, initial public release
-- 0.3 (2012-02-24) bugfixes, single-line mode; insert mode
-- 0.2 (2011-11-28) fixes to undo/redo
-- 0.1 (2010-07-08) initial version
--
-- ADDITIONAL CONTRIBUTORS
--
-- Ulf Winklemann: move-by-word in 1.1
-- Fabian Giesen: secondary key inputs in 1.5
-- Martins Mozeiko: STB_TEXTEDIT_memmove in 1.6
-- Louis Schnellbach: page up/down in 1.14
--
-- Bugfixes:
-- Scott Graham
-- Daniel Keller
-- Omar Cornut
-- Dan Thompson
--
-- USAGE
--
-- The basic concept is that you provide a "string" object which
-- behaves like an array of characters. stb_textedit uses indices to
-- refer to positions in the string, implicitly representing positions
-- in the displayed textedit. This is true for both plain text and
-- rich text; even with rich text stb_truetype interacts with your
-- code as if there was an array of all the displayed characters.
--
-- Symbols you must define for implementation mode (in this port: fields of ImStb, see below):
--
-- STB_TEXTEDIT_STRING the type of object representing a string being edited,
-- typically this is a wrapper object with other data you need
--
-- STB_TEXTEDIT_STRINGLEN(obj) the length of the string (ideally O(1))
-- STB_TEXTEDIT_LAYOUTROW(r,obj,n) returns the results of laying out a line of characters
-- starting from character #n (see discussion below)
-- STB_TEXTEDIT_GETWIDTH(obj,n,i) returns the pixel delta from the xpos of the i'th character
-- to the xpos of the i+1'th char for a line of characters
-- starting at character #n (i.e. accounts for kerning
-- with previous char)
-- STB_TEXTEDIT_GETCHAR(obj,i) returns the i'th character of obj, 1-based in this port
-- STB_TEXTEDIT_NEWLINE the character returned by _GETCHAR() we recognize
-- as manually wordwrapping for end-of-line positioning
--
-- STB_TEXTEDIT_DELETECHARS(obj,i,n) delete n characters starting at i
-- STB_TEXTEDIT_INSERTCHARS(obj,i,c,c_offset,n) try to insert n characters at i (taken from array c starting at c_offset)
-- returns number of characters actually inserted. [DEAR IMGUI]
--
-- STB_TEXTEDIT_K_SHIFT a power of two that is or'd in to a keyboard input to represent the shift key
--
-- STB_TEXTEDIT_K_LEFT keyboard input to move cursor left
-- STB_TEXTEDIT_K_RIGHT keyboard input to move cursor right
-- STB_TEXTEDIT_K_UP keyboard input to move cursor up
-- STB_TEXTEDIT_K_DOWN keyboard input to move cursor down
-- STB_TEXTEDIT_K_PGUP keyboard input to move cursor up a page
-- STB_TEXTEDIT_K_PGDOWN keyboard input to move cursor down a page
-- STB_TEXTEDIT_K_LINESTART keyboard input to move cursor to start of line -- e.g. HOME
-- STB_TEXTEDIT_K_LINEEND keyboard input to move cursor to end of line -- e.g. END
-- STB_TEXTEDIT_K_TEXTSTART keyboard input to move cursor to start of text -- e.g. ctrl-HOME
-- STB_TEXTEDIT_K_TEXTEND keyboard input to move cursor to end of text -- e.g. ctrl-END
-- STB_TEXTEDIT_K_DELETE keyboard input to delete selection or character under cursor
-- STB_TEXTEDIT_K_BACKSPACE keyboard input to delete selection or character left of cursor
-- STB_TEXTEDIT_K_UNDO keyboard input to perform undo
-- STB_TEXTEDIT_K_REDO keyboard input to perform redo
--
-- STB_TEXTEDIT_MOVEWORDLEFT(obj,i) custom handler for WORDLEFT, returns index to move cursor to
-- STB_TEXTEDIT_MOVEWORDRIGHT(obj,i) custom handler for WORDRIGHT, returns index to move cursor to
-- STB_TEXTEDIT_K_WORDLEFT keyboard input to move cursor left one word -- e.g. ctrl-LEFT
-- STB_TEXTEDIT_K_WORDRIGHT keyboard input to move cursor right one word -- e.g. ctrl-RIGHT
--
-- To support UTF-8:
--
-- STB_TEXTEDIT_GETPREVCHARINDEX returns index of previous character
-- STB_TEXTEDIT_GETNEXTCHARINDEX returns index of next character
-- Do NOT define STB_TEXTEDIT_KEYTOTEXT.
-- Instead, call stb_textedit_text() directly for text contents.
--
-- Keyboard input must be encoded as a single integer value; e.g. a character code
-- and some bitflags that represent shift states. to simplify the interface, SHIFT must
-- be a bitflag, so we can test the shifted state of cursor movements to allow selection,
-- i.e. (STB_TEXTEDIT_K_RIGHT|STB_TEXTEDIT_K_SHIFT) should be shifted right-arrow.
--
-- STB_TEXTEDIT_LAYOUTROW returns information about the shape of one displayed
-- row of characters assuming they start on the i'th character--the width and
-- the height and the number of characters consumed. This allows this library
-- to traverse the entire layout incrementally. You need to compute word-wrapping
-- here.
--
-- Each textfield keeps its own insert mode state, which is not how normal
-- applications work. To keep an app-wide insert mode, update/copy the
-- "insert_mode" field of STB_TexteditState before/after calling API functions.
--
-- API (in this port: fields of the table returned by this file)
--
-- initialize_state(state, is_single_line)
--
-- click(str, state, x, y)
-- drag(str, state, x, y)
-- cut(str, state) -> bool
-- paste(str, state, text, len) -> bool
-- key(str, state, key)
-- text(str, state, text, text_len)
--
-- Each of these functions potentially updates the string and updates the
-- state.
--
-- initialize_state:
-- set the textedit state to a known good default state when initially
-- constructing the textedit.
--
-- click:
-- call this with the mouse x,y on a mouse down; it will update the cursor
-- and reset the selection start/end to the cursor point. the x,y must
-- be relative to the text widget, with (0,0) being the top left.
--
-- drag:
-- call this with the mouse x,y on a mouse drag/up; it will update the
-- cursor and the selection end point
--
-- cut:
-- call this to delete the current selection; returns true if there was
-- one. you should FIRST copy the current selection to the system paste buffer.
-- (To copy, just copy the current selection out of the string yourself.)
--
-- paste:
-- call this to paste text at the current cursor point or over the current
-- selection if there is one.
--
-- key:
-- call this for keyboard inputs sent to the textfield. you can use it
-- for "key down" events or for "translated" key events.
--
-- text: (added 2025)
-- call this to directly send text input the textfield, which is required
-- for UTF-8 support, because stb_textedit_key() + STB_TEXTEDIT_KEYTOTEXT()
-- cannot infer text length.
--
--
-- When rendering, you can read the cursor position and selection state from
-- the STB_TexteditState.
--
--
-- Notes:
--
-- This is designed to be usable in IMGUI, so it allows for the possibility of
-- running in an IMGUI that has NOT cached the multi-line layout. For this
-- reason, it provides an interface that is compatible with computing the
-- layout incrementally--we try to make sure we make as few passes through
-- as possible. (For example, to locate the mouse pointer in the text, we
-- could define functions that return the X and Y positions of characters
-- and binary search Y and then X, but if we're doing dynamic layout this
-- will run the layout algorithm many times, so instead we manually search
-- forward in one pass. Similar logic applies to e.g. up-arrow and
-- down-arrow movement.)
--
-- If it's run in a widget that *has* cached the layout, then this is less
-- efficient, but it's not horrible on modern computers. But you wouldn't
-- want to edit million-line files with it.
-- ALL TABLES IN THIS FILE ARE 1-BASED!
-- Character Indexing:
-- idx=n, the n'th char (1-based)
-- Cursor Indexing:
-- idx=n, the position before the n'th char
-- idx=n+1, the position after the n'th char
-- Symbols that must be defined before using this:
local STB_TEXTEDIT_STRINGLEN = ImStb.TEXTEDIT_STRINGLEN
local STB_TEXTEDIT_GETCHAR = ImStb.TEXTEDIT_GETCHAR
local STB_TEXTEDIT_GETWIDTH = ImStb.TEXTEDIT_GETWIDTH
local STB_TEXTEDIT_LAYOUTROW = ImStb.TEXTEDIT_LAYOUTROW
-- [DEAR IMGUI]
-- Functions must be implemented for UTF8 support
-- Code in this file that uses those functions is modified for [DEAR IMGUI] and deviates from the original stb_textedit.
-- There is not necessarily a '[DEAR IMGUI]' at the usage sites.
local IMSTB_TEXTEDIT_GETNEXTCHARINDEX = ImStb.TEXTEDIT_GETNEXTCHARINDEX
local IMSTB_TEXTEDIT_GETPREVCHARINDEX = ImStb.TEXTEDIT_GETPREVCHARINDEX
local STB_TEXTEDIT_MOVEWORDLEFT = ImStb.TEXTEDIT_MOVEWORDLEFT
local STB_TEXTEDIT_MOVEWORDRIGHT = ImStb.TEXTEDIT_MOVEWORDRIGHT
local STB_TEXTEDIT_MOVELINESTART = ImStb.TEXTEDIT_MOVELINESTART
local STB_TEXTEDIT_MOVELINEEND = ImStb.TEXTEDIT_MOVELINEEND
local STB_TEXTEDIT_DELETECHARS = ImStb.TEXTEDIT_DELETECHARS
local STB_TEXTEDIT_INSERTCHARS = ImStb.TEXTEDIT_INSERTCHARS
STB_TEXTEDIT_NEWLINE = 10 -- the character returned by _GETCHAR() we recognize as manually wordwrapping for end-of-line positioning
STB_TEXTEDIT_K_LEFT = 0x200000 -- keyboard input to move cursor left
STB_TEXTEDIT_K_RIGHT = 0x200001 -- keyboard input to move cursor right
STB_TEXTEDIT_K_UP = 0x200002 -- keyboard input to move cursor up
STB_TEXTEDIT_K_DOWN = 0x200003 -- keyboard input to move cursor down
STB_TEXTEDIT_K_LINESTART = 0x200004 -- keyboard input to move cursor to start of line -- e.g. HOME
STB_TEXTEDIT_K_LINEEND = 0x200005 -- keyboard input to move cursor to end of line -- e.g. END
STB_TEXTEDIT_K_TEXTSTART = 0x200006 -- keyboard input to move cursor to start of text -- e.g. ctrl-HOME
STB_TEXTEDIT_K_TEXTEND = 0x200007 -- keyboard input to move cursor to end of text -- e.g. ctrl-END
STB_TEXTEDIT_K_DELETE = 0x200008 -- keyboard input to delete selection or character under cursor
STB_TEXTEDIT_K_BACKSPACE = 0x200009 -- keyboard input to delete selection or character left of cursor
STB_TEXTEDIT_K_UNDO = 0x20000A -- keyboard input to perform undo
STB_TEXTEDIT_K_REDO = 0x20000B -- keyboard input to perform redo
STB_TEXTEDIT_K_WORDLEFT = 0x20000C -- keyboard input to move cursor left one word -- e.g. ctrl-LEFT
STB_TEXTEDIT_K_WORDRIGHT = 0x20000D -- keyboard input to move cursor right one word -- e.g. ctrl-RIGHT
STB_TEXTEDIT_K_PGUP = 0x20000E -- keyboard input to move cursor up a page
STB_TEXTEDIT_K_PGDOWN = 0x20000F -- keyboard input to move cursor down a page
STB_TEXTEDIT_K_SHIFT = 0x400000 -- a power of two that is or'd in to a keyboard input to represent the shift key
IMSTB_TEXTEDIT_UNDOSTATECOUNT = 99 -- the number of undo states to allow
local IMSTB_TEXTEDIT_UNDOCHARCOUNT = 999 -- the number of characters to store in the undo buffer
local IMSTB_TEXTEDIT_memmove = ImStb.TEXTEDIT_memmove
-- forward declarations
local stb_text_undo
local stb_text_redo
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
---
--- STB_TexteditState
---
--- Definition of STB_TexteditState which you should store
--- per-textfield; it includes cursor position, selection state,
--- and undo state.
---
--- @alias IMSTB_TEXTEDIT_POSITIONTYPE int
--- @alias IMSTB_TEXTEDIT_CHARTYPE char
--- @alias STB_TEXTEDIT_KEYTYPE int
--- @class StbUndoRecord
--- @field where IMSTB_TEXTEDIT_POSITIONTYPE
--- @field insert_length IMSTB_TEXTEDIT_POSITIONTYPE
--- @field delete_length IMSTB_TEXTEDIT_POSITIONTYPE
--- @field char_storage int
--- @return StbUndoRecord
--- @nodiscard
local function StbUndoRecord()
return {
-- private data
where = 1,
insert_length = 0,
delete_length = 0,
char_storage = 1
}
end
--- @class StbUndoState
--- @field undo_rec StbUndoRecord[] # size = IMSTB_TEXTEDIT_UNDOSTATECOUNT
--- @field undo_char IMSTB_TEXTEDIT_CHARTYPE[] # size = IMSTB_TEXTEDIT_UNDOCHARCOUNT
--- @field undo_point short # next available slot
--- @field redo_point short # next available slot
--- @field undo_char_point int # next available slot
--- @field redo_char_point int # next available slot
--- @return StbUndoState
--- @nodiscard
local function StbUndoState()
local undo_rec = {}
for i = 1, IMSTB_TEXTEDIT_UNDOSTATECOUNT do
undo_rec[i] = StbUndoRecord()
end
local undo_char = {}
for i = 1, IMSTB_TEXTEDIT_UNDOCHARCOUNT do
undo_char[i] = 0
end
return {
-- private data
undo_rec = undo_rec,
undo_char = undo_char,
undo_point = 1,
redo_point = 1,
undo_char_point = 1,
redo_char_point = 1
}
end
--- @class STB_TexteditState
--- @field cursor int # position of the text cursor within the string
--- @field select_start int # selection start point
--- @field select_end int
--- @field insert_mode bool
--- @field row_count_per_page int
--- @field cursor_at_end_of_line bool # not implemented yet
--- @field initialized bool
--- @field has_preferred_x bool
--- @field single_line bool
--- @field padding1 unsigned_char
--- @field padding2 unsigned_char
--- @field padding3 unsigned_char
--- @field preferred_x float
--- @field undostate StbUndoState
--- @return STB_TexteditState
--- @nodiscard
function STB_TexteditState()
return {
---------------------
--
-- public data
--
cursor = 0,
-- position of the text cursor within the string
select_start = 0, -- selection start point
select_end = 0,
-- selection start and end point in characters; if equal, no selection.
-- note that start may be less than or greater than end (e.g. when
-- dragging the mouse, start is where the initial click was, and you
-- can drag in either direction)
insert_mode = false,
-- each textfield keeps its own insert mode state. to keep an app-wide
-- insert mode, copy this value in/out of the app state
row_count_per_page = 0,
-- page size in number of row.
-- this value MUST be set to >0 for pageup or pagedown in multilines documents.
---------------------
--
-- private data
--
cursor_at_end_of_line = false, -- not implemented yet
initialized = false,
has_preferred_x = false,
single_line = false,
padding1 = 0,
padding2 = 0,
padding3 = 0,
preferred_x = 0.0, -- this determines where the cursor up/down tries to seek to along x
undostate = StbUndoState()
}
end
--- @param s STB_TexteditState
local function STB_TEXT_HAS_SELECTION(s)
return s.select_start ~= s.select_end
end
-------------------------------------------------------------------------------
---
--- StbTexteditRow
---
--- Result of layout query, used by stb_textedit to determine where
--- the text in each row is.
-- result of layout query
--- @class StbTexteditRow
--- @field x0 float # starting x location, end x location (allows for align=right, etc)
--- @field x1 float # end x location
--- @field baseline_y_delta float # position of baseline relative to previous row's baseline
--- @field ymin float # height of row above and below baseline
--- @field ymax float # height of row above and below baseline
--- @field num_chars int
--- @return StbTexteditRow
--- @nodiscard
local function StbTexteditRow()
return {
x0 = 0.0,
x1 = 0.0,
baseline_y_delta = 0.0,
ymin = 0.0,
ymax = 0.0,
num_chars = 0
}
end
--- @param r StbTexteditRow
local function StbTexteditRow_Reset(r)
r.x0 = 0.0
r.x1 = 0.0
r.baseline_y_delta = 0.0
r.ymin = 0.0
r.ymax = 0.0
r.num_chars = 0
end
----------------------------------------
----------------------------------------
---
--- Implementation mode
---
---
-------------------------------------------------------------------------------
---
--- Mouse input handling
---
local stb_text_locate_coord do
-- only create once, reuse later
local r = StbTexteditRow()
--- traverse the layout to locate the nearest character to a display position
--- @param str IMSTB_TEXTEDIT_STRING
--- @param x float
--- @param y float
--- @return int idx
--- @return bool side_on_line
function stb_text_locate_coord(str, x, y)
local n = STB_TEXTEDIT_STRINGLEN(str)
local base_y = 0
local prev_x
StbTexteditRow_Reset(r)
local out_side_on_line = false
-- search rows to find one that straddles 'y'
local i = 1
while i <= n do
STB_TEXTEDIT_LAYOUTROW(r, str, i)
if r.num_chars <= 0 then
return n + 1, out_side_on_line
end
if i == 1 and y < base_y + r.ymin then
return 1, out_side_on_line
end
if y < base_y + r.ymax then
break
end
i = i + r.num_chars
base_y = base_y + r.baseline_y_delta
end
-- below all text, return 'after' last character
if i > n then
out_side_on_line = true
return n + 1, out_side_on_line
end
-- check if it's before the beginning of the line
if x < r.x0 then
return i, out_side_on_line
end
-- check if it's before the end of the line
if x < r.x1 then
-- search characters in row for one that straddles 'x'
prev_x = r.x0
local k = 1
while k <= r.num_chars do
local w = STB_TEXTEDIT_GETWIDTH(str, i, k)
if x < prev_x + w then
out_side_on_line = (k == 1) and false or true
if x < prev_x + w / 2 then
return k + i - 1, out_side_on_line
else
return IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k - 1), out_side_on_line
end
end
prev_x = prev_x + w
k = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k - 1) - i + 1
end
-- shouldn't happen, but if it does, fall through to end-of-line case
end
-- if the last character is a newline, return that. otherwise return 'after' the last character
out_side_on_line = true
if STB_TEXTEDIT_GETCHAR(str, i + r.num_chars - 1) == STB_TEXTEDIT_NEWLINE then
return i + r.num_chars - 1, out_side_on_line
else
return i + r.num_chars, out_side_on_line
end
end
end
local stb_textedit_click do
local r = StbTexteditRow()
-- API click: on mouse down, move the cursor to the clicked location, and reset the selection
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
--- @param x float
--- @param y float
function stb_textedit_click(str, state, x, y)
-- In single-line mode, just always make y = 0. This lets the drag keep working if the mouse
-- goes off the top or bottom of the text
local side_on_line
if state.single_line then
StbTexteditRow_Reset(r)
STB_TEXTEDIT_LAYOUTROW(r, str, 1)
y = r.ymin
end
state.cursor, side_on_line = stb_text_locate_coord(str, x, y)
state.select_start = state.cursor
state.select_end = state.cursor
state.has_preferred_x = false
str.LastMoveDirectionLR = side_on_line and ImGuiDir.Right or ImGuiDir.Left
end
end
local stb_textedit_drag do
local r = StbTexteditRow()
-- API drag: on mouse drag, move the cursor and selection endpoint to the clicked location
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
--- @param x float
--- @param y float
function stb_textedit_drag(str, state, x, y)
local p = 1
local side_on_line
-- In single-line mode, just always make y = 0. This lets the drag keep working if the mouse
-- goes off the top or bottom of the text
if state.single_line then
StbTexteditRow_Reset(r)
STB_TEXTEDIT_LAYOUTROW(r, str, 1)
y = r.ymin
end
if state.select_start == state.select_end then
state.select_start = state.cursor
end
p, side_on_line = stb_text_locate_coord(str, x, y)
state.cursor = p
state.select_end = p
str.LastMoveDirectionLR = (side_on_line ~= false) and ImGuiDir.Right or ImGuiDir.Left
end
end
---------------------------
---------------------------
---
--- Keyboard input handling
---
---
-- forward declarations
local stb_text_makeundo_insert
local stb_text_makeundo_delete
local stb_text_makeundo_replace
--- @class StbFindState
--- @field x float # position of n'th character
--- @field y float
--- @field height float # height of line
--- @field first_char int # first char of row, and length
--- @field length int
--- @field prev_first int # first char of previous row
--- @return StbFindState
local function StbFindState()
return {
x = 0.0, y = 0.0,
height = 0.0,
first_char = 0, length = 0,
prev_first = 0
}
end
--- @param f StbFindState
local function StbFindState_Reset(f)
f.x = 0.0
f.y = 0.0
f.height = 0.0
f.first_char = 0
f.length = 0
f.prev_first = 0
end
local stb_textedit_find_charpos do
local r = StbTexteditRow()
-- find the x/y location of a character, and remember info about the previous row in
-- case we get a move-up event (for page up, we'll have to rescan)
--- @param find StbFindState
--- @param str IMSTB_TEXTEDIT_STRING
--- @param n int
--- @param single_line bool
function stb_textedit_find_charpos(find, str, n, single_line)
StbTexteditRow_Reset(r)
local prev_start = 1
local z = STB_TEXTEDIT_STRINGLEN(str)
local first
if n == z + 1 and single_line then
-- special case if it's at the end (may not be needed?)
STB_TEXTEDIT_LAYOUTROW(r, str, 1)
find.y = 0
find.first_char = 1
find.length = z
find.height = r.ymax - r.ymin
find.x = r.x1
return
end
-- search rows to find the one that straddles character n
find.y = 0
local i = 1
while true do
STB_TEXTEDIT_LAYOUTROW(r, str, i)
if n < i + r.num_chars - 1 then
break
end
if str.LastMoveDirectionLR == ImGuiDir.Right and str.Stb.cursor > 1 and str.Stb.cursor == i + r.num_chars and STB_TEXTEDIT_GETCHAR(str, i + r.num_chars - 1) ~= STB_TEXTEDIT_NEWLINE then -- [DEAR IMGUI] Wrapping point handling
break
end
if (i - 1) + r.num_chars == z and z > 0 and STB_TEXTEDIT_GETCHAR(str, z) ~= STB_TEXTEDIT_NEWLINE then -- [DEAR IMGUI] special handling for last line
break -- [DEAR IMGUI]
end
prev_start = i
i = i + r.num_chars
find.y = find.y + r.baseline_y_delta
if i == z + 1 then -- [DEAR IMGUI]
r.num_chars = 0 -- [DEAR IMGUI]
break -- [DEAR IMGUI]
end
end
find.first_char = i
first = i
find.length = r.num_chars
find.height = r.ymax - r.ymin
find.prev_first = prev_start
-- now scan to find xpos
find.x = r.x0
i = 1
while first + i <= n + 1 do
find.x = find.x + STB_TEXTEDIT_GETWIDTH(str, first, i)
i = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, first + i - 1) - first + 1
end
end
end
-- make the selection/cursor state valid if client altered the string
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
local function stb_textedit_clamp(str, state)
local n = STB_TEXTEDIT_STRINGLEN(str)
if STB_TEXT_HAS_SELECTION(state) then
if state.select_start > n + 1 then state.select_start = n + 1 end
if state.select_end > n + 1 then state.select_end = n + 1 end
-- if clamping forced them to be equal, move the cursor to match
if state.select_start == state.select_end then
state.cursor = state.select_start
end
end
if state.cursor > n + 1 then state.cursor = n + 1 end
end
-- delete characters while updating undo
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
--- @param where int
--- @param len int
local function stb_textedit_delete(str, state, where, len)
stb_text_makeundo_delete(str, state, where, len)
STB_TEXTEDIT_DELETECHARS(str, where, len)
state.has_preferred_x = false
end
-- delete the section
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
local function stb_textedit_delete_selection(str, state)
stb_textedit_clamp(str, state)
if STB_TEXT_HAS_SELECTION(state) then
if state.select_start < state.select_end then
stb_textedit_delete(str, state, state.select_start, state.select_end - state.select_start)
state.cursor = state.select_start
state.select_end = state.cursor
else
stb_textedit_delete(str, state, state.select_end, state.select_start - state.select_end)
state.cursor = state.select_end
state.select_start = state.cursor
end
state.has_preferred_x = false
end
end
-- canoncialize the selection so start <= end
--- @param state STB_TexteditState
local function stb_textedit_sortselection(state)
if state.select_end < state.select_start then
local temp = state.select_end
state.select_end = state.select_start
state.select_start = temp
end
end
-- move cursor to first character of selection
--- @param state STB_TexteditState
local function stb_textedit_move_to_first(state)
if STB_TEXT_HAS_SELECTION(state) then
stb_textedit_sortselection(state)
state.cursor = state.select_start
state.select_end = state.select_start
state.has_preferred_x = false
end
end
-- move cursor to last character of selection
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
local function stb_textedit_move_to_last(str, state)
if STB_TEXT_HAS_SELECTION(state) then
stb_textedit_sortselection(state)
stb_textedit_clamp(str, state)
state.cursor = state.select_end
state.select_start = state.select_end
state.has_preferred_x = false
end
end
-- update selection and cursor to match each other
--- @param state STB_TexteditState
local function stb_textedit_prep_selection_at_cursor(state)
if not STB_TEXT_HAS_SELECTION(state) then
state.select_start = state.cursor
state.select_end = state.cursor
else
state.cursor = state.select_end
end
end
-- API cut: delete selection
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
--- @return bool
local function stb_textedit_cut(str, state)
if STB_TEXT_HAS_SELECTION(state) then
stb_textedit_delete_selection(str, state) -- implicitly clamps
state.has_preferred_x = false
return true
end
return false
end
-- API paste: replace existing selection with passed-in text
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
--- @param text IMSTB_TEXTEDIT_CHARTYPE[]
--- @param len int
--- @return bool
local function stb_textedit_paste_internal(str, state, text, len)
-- if there's a selection, the paste should delete it
stb_textedit_clamp(str, state)
stb_textedit_delete_selection(str, state)
-- try to insert the characters
len = STB_TEXTEDIT_INSERTCHARS(str, state.cursor, text, 1, len)
if len then
stb_text_makeundo_insert(state, state.cursor, len)
state.cursor = state.cursor + len
state.has_preferred_x = false
return true
end
-- note: paste failure will leave deleted selection, may be restored with an undo (see https://github.com/nothings/stb/issues/734 for details)
return false
end
-- API key: process text input
-- [DEAR IMGUI] Added stb_textedit_text(), extracted out and called by stb_textedit_key() for backward compatibility.
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
--- @param text IMSTB_TEXTEDIT_CHARTYPE[]
--- @param text_len int
local function stb_textedit_text(str, state, text, text_len)
-- can't add newline in single-line mode
if text[1] == STB_TEXTEDIT_NEWLINE and state.single_line then
return
end
if state.insert_mode and not STB_TEXT_HAS_SELECTION(state) and state.cursor < STB_TEXTEDIT_STRINGLEN(str) then
stb_text_makeundo_replace(str, state, state.cursor, 1, 1)
STB_TEXTEDIT_DELETECHARS(str, state.cursor, 1)
text_len = STB_TEXTEDIT_INSERTCHARS(str, state.cursor, text, 1, text_len)
if text_len then
state.cursor = state.cursor + text_len
state.has_preferred_x = false
end
else
stb_textedit_delete_selection(str, state) -- implicitly clamps
text_len = STB_TEXTEDIT_INSERTCHARS(str, state.cursor, text, 1, text_len)
if text_len then
stb_text_makeundo_insert(state, state.cursor, text_len)
state.cursor = state.cursor + text_len
state.has_preferred_x = false
end
end
end
local stb_textedit_key do
local find = StbFindState()
local row = StbTexteditRow()
-- API key: process a keyboard input
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
--- @param key STB_TEXTEDIT_KEYTYPE
function stb_textedit_key(str, state, key)
if key == STB_TEXTEDIT_K_UNDO then
stb_text_undo(str, state)
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_REDO then
stb_text_redo(str, state)
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_LEFT then
-- if currently there's a selection, move cursor to start of selection
if STB_TEXT_HAS_SELECTION(state) then
stb_textedit_move_to_first(state)
else
if state.cursor > 1 then
state.cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state.cursor)
end
end
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_RIGHT then
-- if currently there's a selection, move cursor to end of selection
if STB_TEXT_HAS_SELECTION(state) then
stb_textedit_move_to_last(str, state)
else
state.cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state.cursor)
end
stb_textedit_clamp(str, state)
state.has_preferred_x = false
elseif key == bit32.bor(STB_TEXTEDIT_K_LEFT, STB_TEXTEDIT_K_SHIFT) then
stb_textedit_clamp(str, state)
stb_textedit_prep_selection_at_cursor(state)
-- move selection left
if state.select_end > 1 then
state.select_end = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state.select_end)
end
state.cursor = state.select_end
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_WORDLEFT then
-- if currently there's a selection, move cursor to start of selection
if STB_TEXT_HAS_SELECTION(state) then
stb_textedit_move_to_first(state)
else
state.cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state.cursor)
stb_textedit_clamp(str, state)
end
elseif key == bit32.bor(STB_TEXTEDIT_K_WORDLEFT, STB_TEXTEDIT_K_SHIFT) then
if not STB_TEXT_HAS_SELECTION(state) then
stb_textedit_prep_selection_at_cursor(state)
end
state.cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state.cursor)
state.select_end = state.cursor
stb_textedit_clamp(str, state)
elseif key == STB_TEXTEDIT_K_WORDRIGHT then
-- if currently there's a selection, move cursor to end of selection
if STB_TEXT_HAS_SELECTION(state) then
stb_textedit_move_to_last(str, state)
else
state.cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state.cursor)
stb_textedit_clamp(str, state)
end
elseif key == bit32.bor(STB_TEXTEDIT_K_WORDRIGHT, STB_TEXTEDIT_K_SHIFT) then
if not STB_TEXT_HAS_SELECTION(state) then
stb_textedit_prep_selection_at_cursor(state)
end
state.cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state.cursor)
state.select_end = state.cursor
stb_textedit_clamp(str, state)
elseif key == bit32.bor(STB_TEXTEDIT_K_RIGHT, STB_TEXTEDIT_K_SHIFT) then
stb_textedit_prep_selection_at_cursor(state)
-- move selection right
state.select_end = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state.select_end)
stb_textedit_clamp(str, state)
state.cursor = state.select_end
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_DOWN
or key == bit32.bor(STB_TEXTEDIT_K_DOWN, STB_TEXTEDIT_K_SHIFT)
or key == STB_TEXTEDIT_K_PGDOWN
or key == bit32.bor(STB_TEXTEDIT_K_PGDOWN, STB_TEXTEDIT_K_SHIFT) then
StbFindState_Reset(find)
StbTexteditRow_Reset(row)
local sel = (bit32.band(key, STB_TEXTEDIT_K_SHIFT) ~= 0)
local is_page = (bit32.band(key, bit32.bnot(STB_TEXTEDIT_K_SHIFT)) == STB_TEXTEDIT_K_PGDOWN)
local row_count = is_page and state.row_count_per_page or 1
if not is_page and state.single_line then
-- on windows, up&down in single-line behave like left&right
key = bit32.bor(STB_TEXTEDIT_K_RIGHT, bit32.band(key, STB_TEXTEDIT_K_SHIFT))
return stb_textedit_key(str, state, key) -- was `goto retry`
end
if sel then
stb_textedit_prep_selection_at_cursor(state)
elseif STB_TEXT_HAS_SELECTION(state) then
stb_textedit_move_to_last(str, state)
end
-- compute current position of cursor point
stb_textedit_clamp(str, state)
stb_textedit_find_charpos(find, str, state.cursor, state.single_line)
for j = 1, row_count do
local goal_x = state.has_preferred_x and state.preferred_x or find.x
local x
local start = find.first_char + find.length
if find.length == 0 then
break
end
-- [DEAR IMGUI]
-- going down while being on the last line shouldn't bring us to that line end
--if STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) ~= STB_TEXTEDIT_NEWLINE then
-- break
--end
-- now find character position down a row
state.cursor = start
STB_TEXTEDIT_LAYOUTROW(row, str, state.cursor)
x = row.x0
local i = 1
while i <= row.num_chars do
local dx = STB_TEXTEDIT_GETWIDTH(str, start, i)
local next = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state.cursor)
x = x + dx
if x > goal_x then
break
end
i = i + (next - state.cursor)
state.cursor = next
end
stb_textedit_clamp(str, state)
if state.cursor == find.first_char + find.length then
str.LastMoveDirectionLR = ImGuiDir.Left
end
state.has_preferred_x = true
state.preferred_x = goal_x
if sel then
state.select_end = state.cursor
end
-- go to next line
find.first_char = find.first_char + find.length
find.length = row.num_chars
end
elseif key == STB_TEXTEDIT_K_UP
or key == bit32.bor(STB_TEXTEDIT_K_UP, STB_TEXTEDIT_K_SHIFT)
or key == STB_TEXTEDIT_K_PGUP
or key == bit32.bor(STB_TEXTEDIT_K_PGUP, STB_TEXTEDIT_K_SHIFT) then
StbFindState_Reset(find)
StbTexteditRow_Reset(row)
local sel = (bit32.band(key, STB_TEXTEDIT_K_SHIFT) ~= 0)
local is_page = (bit32.band(key, bit32.bnot(STB_TEXTEDIT_K_SHIFT)) == STB_TEXTEDIT_K_PGUP)
local row_count = is_page and state.row_count_per_page or 1
if not is_page and state.single_line then
-- on windows, up&down become left&right
key = bit32.bor(STB_TEXTEDIT_K_LEFT, bit32.band(key, STB_TEXTEDIT_K_SHIFT))
return stb_textedit_key(str, state, key) -- was `goto retry`
end
if sel then
stb_textedit_prep_selection_at_cursor(state)
elseif STB_TEXT_HAS_SELECTION(state) then
stb_textedit_move_to_first(state)
end
-- compute current position of cursor point
stb_textedit_clamp(str, state)
stb_textedit_find_charpos(find, str, state.cursor, state.single_line)
for j = 1, row_count do
local goal_x
if state.has_preferred_x then
goal_x = state.preferred_x
else
goal_x = find.x
end
local x
-- can only go up if there's a previous row
if find.prev_first == find.first_char then
break
end
-- now find character position up a row
state.cursor = find.prev_first
STB_TEXTEDIT_LAYOUTROW(row, str, state.cursor)
x = row.x0
local i = 1
while i <= row.num_chars do
local dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i)
local next = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state.cursor)
x = x + dx
if x > goal_x then
break
end
i = i + (next - state.cursor)
state.cursor = next
end
stb_textedit_clamp(str, state)
if state.cursor == find.first_char then
str.LastMoveDirectionLR = ImGuiDir.Right
elseif state.cursor == find.prev_first then
str.LastMoveDirectionLR = ImGuiDir.Left
end
state.has_preferred_x = true
state.preferred_x = goal_x
if sel then
state.select_end = state.cursor
end
-- go to previous line
-- (we need to scan previous line the hard way. maybe we could expose this as a new API function?)
local prev_scan = find.prev_first > 1 and find.prev_first - 1 or 1
while prev_scan > 1 do
local prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, prev_scan)
if STB_TEXTEDIT_GETCHAR(str, prev) == STB_TEXTEDIT_NEWLINE then
break
end
prev_scan = prev
end
find.first_char = find.prev_first
find.prev_first = STB_TEXTEDIT_MOVELINESTART(str, state, prev_scan)
end
elseif key == STB_TEXTEDIT_K_DELETE
or key == bit32.bor(STB_TEXTEDIT_K_DELETE, STB_TEXTEDIT_K_SHIFT) then
if STB_TEXT_HAS_SELECTION(state) then
stb_textedit_delete_selection(str, state)
else
local n = STB_TEXTEDIT_STRINGLEN(str)
if state.cursor <= n then
stb_textedit_delete(str, state, state.cursor, IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state.cursor) - state.cursor)
end
end
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_BACKSPACE
or key == bit32.bor(STB_TEXTEDIT_K_BACKSPACE, STB_TEXTEDIT_K_SHIFT) then
if STB_TEXT_HAS_SELECTION(state) then
stb_textedit_delete_selection(str, state)
else
stb_textedit_clamp(str, state)
if state.cursor > 1 then
local prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state.cursor)
stb_textedit_delete(str, state, prev, state.cursor - prev)
state.cursor = prev
end
end
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_TEXTSTART then
state.cursor = 1
state.select_start = 1
state.select_end = 1
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_TEXTEND then
state.cursor = STB_TEXTEDIT_STRINGLEN(str) + 1
state.select_start = 1
state.select_end = 1
state.has_preferred_x = false
elseif key == bit32.bor(STB_TEXTEDIT_K_TEXTSTART, STB_TEXTEDIT_K_SHIFT) then
stb_textedit_prep_selection_at_cursor(state)
state.cursor = 1
state.select_end = 1
state.has_preferred_x = false
elseif key == bit32.bor(STB_TEXTEDIT_K_TEXTEND, STB_TEXTEDIT_K_SHIFT) then
stb_textedit_prep_selection_at_cursor(state)
state.cursor = STB_TEXTEDIT_STRINGLEN(str) + 1
state.select_end = state.cursor
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_LINESTART then
stb_textedit_clamp(str, state)
stb_textedit_move_to_first(state)
state.cursor = STB_TEXTEDIT_MOVELINESTART(str, state, state.cursor)
state.has_preferred_x = false
elseif key == STB_TEXTEDIT_K_LINEEND then
stb_textedit_clamp(str, state)
stb_textedit_move_to_last(str, state)
state.cursor = STB_TEXTEDIT_MOVELINEEND(str, state, state.cursor)
state.has_preferred_x = false
elseif key == bit32.bor(STB_TEXTEDIT_K_LINESTART, STB_TEXTEDIT_K_SHIFT) then
stb_textedit_clamp(str, state)
stb_textedit_prep_selection_at_cursor(state)
state.cursor = STB_TEXTEDIT_MOVELINESTART(str, state, state.cursor)
state.select_end = state.cursor
state.has_preferred_x = false
elseif key == bit32.bor(STB_TEXTEDIT_K_LINEEND, STB_TEXTEDIT_K_SHIFT) then
stb_textedit_clamp(str, state)
stb_textedit_prep_selection_at_cursor(state)
state.cursor = STB_TEXTEDIT_MOVELINEEND(str, state, state.cursor)
state.select_end = state.cursor
state.has_preferred_x = false
end
end
end
-----------------------------------------------------
-----------------------------------------------------
---
--- Undo processing
---
--- @OPTIMIZE: the undo/redo buffer should be circular
--- @param state StbUndoState
local function stb_textedit_flush_redo(state)
state.redo_point = IMSTB_TEXTEDIT_UNDOSTATECOUNT + 1
state.redo_char_point = IMSTB_TEXTEDIT_UNDOCHARCOUNT + 1
end
-- discard the oldest entry in the undo list
--- @param state StbUndoState
local function stb_textedit_discard_undo(state)
if state.undo_point > 0 then
-- if the 1th undo state has characters, clean those up
if state.undo_rec[1].char_storage >= 0 then
local n = state.undo_rec[1].insert_length
-- delete n characters from all other records
state.undo_char_point = state.undo_char_point - n
IMSTB_TEXTEDIT_memmove(state.undo_char, 1, state.undo_char, n + 1, state.undo_char_point - 1)
for i = 1, state.undo_point do
if state.undo_rec[i].char_storage >= 0 then
state.undo_rec[i].char_storage = state.undo_rec[i].char_storage - n -- @OPTIMIZE: get rid of char_storage and infer it
end
end
end
state.undo_point = state.undo_point - 1
IMSTB_TEXTEDIT_memmove(state.undo_rec, 1, state.undo_rec, 2, state.undo_point - 1)
end
end
-- discard the oldest entry in the redo list--it's bad if this
-- ever happens, but because undo & redo have to store the actual
-- characters in different cases, the redo character buffer can
-- fill up even though the undo buffer didn't
--- @param state StbUndoState
local function stb_textedit_discard_redo(state)
local k = IMSTB_TEXTEDIT_UNDOSTATECOUNT
if state.redo_point <= k then
-- if the k'th undo state has characters, clean those up
if state.undo_rec[k].char_storage >= 1 then
local n = state.undo_rec[k].insert_length
-- move the remaining redo character data to the end of the buffer
state.redo_char_point = state.redo_char_point + n
IMSTB_TEXTEDIT_memmove(state.undo_char, state.redo_char_point, state.undo_char, state.redo_char_point - n, IMSTB_TEXTEDIT_UNDOCHARCOUNT - state.redo_char_point + 1)
-- adjust the position of all the other records to account for above memmove
for i = state.redo_point, k - 1 do
if state.undo_rec[i].char_storage >= 1 then
state.undo_rec[i].char_storage = state.undo_rec[i].char_storage + n
end
end
end
-- now move all the redo records towards the end of the buffer; the first one is at 'redo_point'
-- [DEAR IMGUI]
local move_size = IMSTB_TEXTEDIT_UNDOSTATECOUNT - state.redo_point - 1
IMSTB_TEXTEDIT_memmove(state.undo_rec, state.redo_point + 1, state.undo_rec, state.redo_point, move_size)
-- now move redo_point to point to the new one
state.redo_point = state.redo_point + 1
end
end
--- @param state StbUndoState
--- @param numchars int
local function stb_text_create_undo_record(state, numchars)
-- any time we create a new undo record, we discard redo
stb_textedit_flush_redo(state)
-- if we have no free records, we have to make room, by sliding the
-- existing records down
if state.undo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT then
stb_textedit_discard_undo(state)
end
-- if the characters to store won't possibly fit in the buffer, we can't undo
if numchars > IMSTB_TEXTEDIT_UNDOCHARCOUNT then
state.undo_point = 1
state.undo_char_point = 1
return nil
end
-- if we don't have enough free characters in the buffer, we have to make room
while state.undo_char_point + numchars > IMSTB_TEXTEDIT_UNDOCHARCOUNT do
stb_textedit_discard_undo(state)
end
local ret = state.undo_rec[state.undo_point]
state.undo_point = state.undo_point + 1
return ret
end
--- @param state StbUndoState
--- @param pos int
--- @param insert_len int
--- @param delete_len int
--- @return int? # index into undostate.undo_char[]
local function stb_text_createundo(state, pos, insert_len, delete_len)
local r = stb_text_create_undo_record(state, insert_len)
if r == nil then
return nil
end
r.where = pos
r.insert_length = insert_len
r.delete_length = delete_len
if insert_len == 0 then
r.char_storage = -1
return nil
else
r.char_storage = state.undo_char_point
state.undo_char_point = state.undo_char_point + insert_len
return r.char_storage
end
end
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
function stb_text_undo(str, state)
local s = state.undostate
if s.undo_point == 1 then
return
end
-- we need to do two things: apply the undo record, and create a redo record
local u = s.undo_rec[s.undo_point - 1]
local r = s.undo_rec[s.redo_point - 1]
r.char_storage = -1
r.insert_length = u.delete_length
r.delete_length = u.insert_length
r.where = u.where
if u.delete_length > 0 then
-- if the undo record says to delete characters, then the redo record will
-- need to re-insert the characters that get deleted, so we need to store
-- them.
-- there are three cases:
-- there's enough room to store the characters
-- characters stored for *redoing* don't leave room for redo
-- characters stored for *undoing* don't leave room for redo
-- if the last is true, we have to bail
if s.undo_char_point + u.delete_length > IMSTB_TEXTEDIT_UNDOCHARCOUNT then
-- the undo records take up too much character space; there's no space to store the redo characters
r.insert_length = 0
else
-- there's definitely room to store the characters eventually
while s.undo_char_point + u.delete_length > s.redo_char_point do
-- should never happen:
if s.redo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT + 1 then
return
end
-- there's currently not enough room, so discard a redo record
stb_textedit_discard_redo(s)
end
r = s.undo_rec[s.redo_point - 1]
r.char_storage = s.redo_char_point - u.delete_length
s.redo_char_point = s.redo_char_point - u.delete_length
-- now save the characters
for i = 0, u.delete_length - 1 do
s.undo_char[r.char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i)
end
end
-- now we can carry out the deletion
STB_TEXTEDIT_DELETECHARS(str, u.where, u.delete_length)
end
-- check type of recorded action:
if u.insert_length > 0 then
-- easy case: was a deletion, so we need to insert n characters
u.insert_length = STB_TEXTEDIT_INSERTCHARS(str, u.where, s.undo_char, u.char_storage, u.insert_length)
s.undo_char_point = s.undo_char_point - u.insert_length
end
state.cursor = u.where + u.insert_length
s.undo_point = s.undo_point - 1
s.redo_point = s.redo_point - 1
end
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
function stb_text_redo(str, state)
local s = state.undostate
if s.undo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT + 1 then
return
end
-- we need to do two things: apply the redo record, and create an undo record
local u = s.undo_rec[s.undo_point]
local r = s.undo_rec[s.redo_point]
-- we KNOW there must be room for the undo record, because the redo record
-- was derived from an undo record
u.delete_length = r.insert_length
u.insert_length = r.delete_length
u.where = r.where
u.char_storage = -1
if r.delete_length > 0 then
-- the redo record requires us to delete characters, so the undo record
-- needs to store the characters
if s.undo_char_point + u.insert_length > s.redo_char_point then
u.insert_length = 0
u.delete_length = 0
else
u.char_storage = s.undo_char_point
s.undo_char_point = s.undo_char_point + u.insert_length
-- now save the characters
for i = 0, u.insert_length - 1 do
s.undo_char[u.char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i)
end
end
STB_TEXTEDIT_DELETECHARS(str, r.where, r.delete_length)
end
if r.insert_length ~= 0 then
-- easy case: need to insert n characters
r.insert_length = STB_TEXTEDIT_INSERTCHARS(str, r.where, s.undo_char, r.char_storage, r.insert_length)
s.redo_char_point = s.redo_char_point + r.insert_length
end
state.cursor = r.where + r.insert_length
s.undo_point = s.undo_point + 1
s.redo_point = s.redo_point + 1
end
--- @param state STB_TexteditState
--- @param where int
--- @param length int
function stb_text_makeundo_insert(state, where, length)
stb_text_createundo(state.undostate, where, 0, length)
end
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
--- @param where int
--- @param length int
function stb_text_makeundo_delete(str, state, where, length)
local s = state.undostate
local p = stb_text_createundo(s, where, length, 0)
if p then
for i = 0, length - 1 do
s.undo_char[p + i] = STB_TEXTEDIT_GETCHAR(str, where + i)
end
end
end
--- @param str IMSTB_TEXTEDIT_STRING
--- @param state STB_TexteditState
--- @param where int
--- @param old_length int
--- @param new_length int
function stb_text_makeundo_replace(str, state, where, old_length, new_length)
local s = state.undostate
local p = stb_text_createundo(s, where, old_length, new_length)
if p then
for i = 0, old_length - 1 do
s.undo_char[p + i] = STB_TEXTEDIT_GETCHAR(str, where + i)
end
end
end
-- reset the state to default
--- @param state STB_TexteditState
--- @param is_single_line bool
local function stb_textedit_clear_state(state, is_single_line)
state.undostate.undo_point = 1
state.undostate.undo_char_point = 1
state.undostate.redo_point = IMSTB_TEXTEDIT_UNDOSTATECOUNT + 1
state.undostate.redo_char_point = IMSTB_TEXTEDIT_UNDOCHARCOUNT + 1
state.select_end = 1
state.select_start = 1
state.cursor = 1
state.has_preferred_x = false
state.preferred_x = 0
state.cursor_at_end_of_line = false
state.initialized = true
state.single_line = is_single_line
state.insert_mode = false
state.row_count_per_page = 0
end
return {
click = stb_textedit_click,
drag = stb_textedit_drag,
createundo = stb_text_createundo,
initialize_state = stb_textedit_clear_state, -- API initialize
text = stb_textedit_text,
key = stb_textedit_key,
makeundo_replace = stb_text_makeundo_replace,
clamp = stb_textedit_clamp,
prep_selection_at_cursor = stb_textedit_prep_selection_at_cursor,
cut = stb_textedit_cut,
paste = stb_textedit_paste_internal,
HAS_SELECTION = STB_TEXT_HAS_SELECTION
}
-------------------------------------------------------------------------------
-- This software is available under 2 licenses -- choose whichever you prefer.
-------------------------------------------------------------------------------
-- ALTERNATIVE A - MIT License
-- Copyright (c) 2017 Sean Barrett
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
-------------------------------------------------------------------------------
-- ALTERNATIVE B - Public Domain (www.unlicense.org)
-- This is free and unencumbered software released into the public domain.
-- Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
-- software, either in source code form or as a compiled binary, for any purpose,
-- commercial or non-commercial, and by any means.
-- In jurisdictions that recognize copyright laws, the author or authors of this
-- software dedicate any and all copyright interest in the software to the public
-- domain. We make this dedication for the benefit of the public at large and to
-- the detriment of our heirs and successors. We intend this dedication to be an
-- overt act of relinquishment in perpetuity of all present and future rights to
-- this software under copyright law.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-- ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-------------------------------------------------------------------------------