Skip to content

Commit 4228ad6

Browse files
authored
Implemented rangeLimit for input / And NoErase (#151)
* Implemented rangeLimit for input / And NoErase Issue #149 - implemented rangeLimit as an int parameter for the insert proc - implemented overWrite as a boolean parameter for the insert proc - added NoErase to the delete/backspace logic of the input widget - fixed the updateInput override in tinput2.nim - ensured all tests pass in ttextboxes.nim * Added unsaved documentation * Corrected vertical cursor movement during selection / improved docs Offset was being calculated relative to one side of the selection rather than the cursorPos. * Removed dead parameter from findLine / cleaned docs
1 parent e44eaad commit 4228ad6

File tree

5 files changed

+89
-89
lines changed

5 files changed

+89
-89
lines changed

src/figuro/ui/textboxes.nim

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,8 @@ proc runeAtCursor*(self: TextBox): Rune =
9292
return Rune(0)
9393
result = self.layout.runes[self.clamped(Left, 0, inclusive=false)]
9494

95-
proc findLine*(self: TextBox, down: bool, select: bool = false): int =
96-
## Finds the index of the line in self.layout.lines that contains the
97-
## relevant cursor/selection point.
98-
## - `down`: Indicates the direction of intended cursor movement (true for down, false for up).
99-
## Used when `select` is false to determine which end of selection to check.
100-
## - `select`: True if the selection is currently being grown (e.g., Shift + Arrow).
101-
## Defaults to false.
95+
proc findLine*(self: TextBox): int =
96+
## Returns the line the cursor is on.
10297

10398
result = -1 # Default to -1 if no line is found
10499

@@ -108,16 +103,7 @@ proc findLine*(self: TextBox, down: bool, select: bool = false): int =
108103
if self.layout.lines.len == 0:
109104
return -1
110105

111-
var charPosToFind: int
112-
if select:
113-
charPosToFind = self.cursorPos
114-
else:
115-
if down:
116-
charPosToFind = self.selectionRange.b
117-
else:
118-
charPosToFind = self.selectionRange.a
119-
120-
let clampedCharPos = clamp(charPosToFind, 0, self.runes().len())
106+
let clampedCharPos = clamp(self.cursorPos, 0, self.runes().len())
121107

122108
for idx, lineSlice in self.layout.lines:
123109
# Standard case: character position is within the rune indices of the line.
@@ -216,13 +202,14 @@ proc updateSelection*(self: var TextBox) =
216202
rect.h = (rhs.y + rhs.h) - lhs.y
217203
self.selectionRects.add rect.descaled()
218204

219-
proc growSelection*(self: var TextBox) =
205+
proc growSelection(self: var TextBox) =
220206
self.selectionRange.a = min(self.cursorPos, self.anchor)
221207
self.selectionRange.b = max(self.cursorPos, self.anchor)
222208
if not self.selectionExists: self.selectionExists = true
223209
self.updateSelection()
224210

225211
proc clearSelection*(self: var TextBox) =
212+
## Clears any existing selection.
226213
self.anchor = self.cursorPos
227214
self.selectionRange.a = self.cursorPos
228215
self.selectionRange.b = self.cursorPos
@@ -248,20 +235,20 @@ proc updateCursor*(self: var TextBox) =
248235
self.cursorRect = cursor.descaled()
249236

250237
proc placeCursor*(self: var TextBox, pos: int, select = false) =
251-
# Places the keyboard cursor at the specified position.
252-
# Clears the selection and brings the anchor along unless
253-
# clearSelection is set to false.
238+
# Places the keyboard cursor at the specified position (`pos`).
239+
#
240+
# If `select` is false (default), then the selection is cleared
241+
# and the archor is moved with the cursor. Otherwise, a selection
242+
# will be created.
254243
self.cursorPos = clamp(pos, 0, self.runes().len())
255244
self.updateCursor()
256245
if select: self.growSelection()
257246
else: self.clearSelection()
258247

259-
proc shiftCursorDown*(self: var TextBox, select = false): int =
260-
## Move cursor or selection down one line.
261-
## - `select`: If true, extends the selection downwards.
262-
## If false, moves the cursor and clears any existing selection.
248+
proc shiftCursorDown(self: var TextBox): int =
249+
## Returns the target position for a downward movement of the cursor.
263250

264-
let presentLineIdx = self.findLine(down = true, select = select)
251+
let presentLineIdx = self.findLine()
265252

266253
# If findLine returns -1 (e.g., empty layout), do nothing.
267254
if presentLineIdx == -1:
@@ -282,20 +269,17 @@ proc shiftCursorDown*(self: var TextBox, select = false): int =
282269
let nextLineSlice = self.layout.lines[nextLineIdx]
283270

284271
# Calculate the horizontal offset (difference in rune indices)
285-
# from the start of the current line to the cursor's Right edge.
286-
let horizontalOffset = self.clamped(Right) - currentLineStartIdx
272+
let horizontalOffset = self.cursorPos - currentLineStartIdx
287273

288274
# Calculate the target rune index on the next line.
289275
# Add the horizontal offset to the start of the next line.
290276
# Ensure the target index doesn't go beyond the end index of the next line.
291277
return min(nextLineSlice.a + horizontalOffset, nextLineSlice.b)
292278

293-
proc shiftCursorUp*(self: var TextBox, select = false): int =
294-
## Move cursor or selection up one line.
295-
## - `select`: If true, extends the selection upwards.
296-
## If false, moves the cursor and clears any existing selection.
279+
proc shiftCursorUp(self: var TextBox): int =
280+
## Returns the target position for a upward movement of the cursor.
297281

298-
let presentLineIdx = self.findLine(down = false, select = select)
282+
let presentLineIdx = self.findLine()
299283
if presentLineIdx == -1:
300284
return
301285

@@ -305,26 +289,27 @@ proc shiftCursorUp*(self: var TextBox, select = false): int =
305289
else:
306290
let previousLineIdx = clamp(presentLineIdx - 1, 0, self.layout.lines.high)
307291
let previousLineSlice = self.layout.lines[previousLineIdx]
308-
let horizontalOffset = self.clamped(Left) - currentLineStartIdx
292+
let horizontalOffset = self.cursorPos - currentLineStartIdx
309293
return min(previousLineSlice.a + horizontalOffset, previousLineSlice.b)
310294

311295
proc shiftCursor*(self: var TextBox,
312-
orientation: Orient,
313-
select = false) =
314-
# Shifts the keyboard cursor based on an orientation.
315-
# Options include: Right, Left, Up, Down,
316-
# Beginning, TheEnd, PreviousWord, NextWord.
317-
# Clears the selection and brings the anchor along unless
318-
# select is set to true.
296+
orientation: Orient,
297+
select = false) =
298+
## Shifts the keyboard cursor based on an `orientation`.
299+
##
300+
## Orientations include: Right, Left, Up, Down,
301+
## Beginning, TheEnd, PreviousWord, NextWord.
302+
## If `select` is true, the selection is extended.
303+
## Otherwise, any existing selection is cleared.
319304
let pos: int = case orientation
320305
of Right: self.cursorPos + 1
321306
of Left: self.cursorPos - 1
322307
of NextWord: self.findNextWord()
323308
of PreviousWord: self.findPrevWord()
324309
of Beginning: 0
325310
of TheEnd: self.runes().len()
326-
of Up: self.shiftCursorUp(select)
327-
of Down: self.shiftCursorDown(select)
311+
of Up: self.shiftCursorUp()
312+
of Down: self.shiftCursorDown()
328313
self.cursorPos = clamp(pos, 0, self.runes().len())
329314
self.updateCursor()
330315
if select: self.growSelection()
@@ -386,28 +371,50 @@ proc delete*(self: var TextBox, orientation: Orient) =
386371
self.placeCursor(idx)
387372
else: discard
388373

389-
proc insert*(self: var TextBox, rune: Rune) =
390-
374+
proc insert*(self: var TextBox,
375+
rune: Rune,
376+
overWrite = false,
377+
rangeLimit = 0) =
378+
## Inserts a rune at the current cursor position.
379+
##
380+
## overWrite: If set to true, then replace the rune in front of cursorPos.
381+
## rangeLimit: If set to a number larger than 0,
382+
## then this function will not insert a rune once there are that many runes.
391383
if self.selectionExists:
392384
self.deleteSelected()
393385

394-
if Overwrite in self.opts:
386+
# 1. no overWrite, no rangeLimit
387+
if not overWrite and rangeLimit == 0:
388+
self.runes.insert(rune, self.cursorPos)
389+
# 2. no overWrite, yes rangeLimit
390+
elif not overWrite and rangeLimit > 0:
391+
if rangeLimit > self.runes.len:
392+
self.runes.insert(rune, self.cursorPos)
393+
# 3. yes overWrite, no rangeLimit
394+
elif overWrite and rangeLimit == 0:
395395
if self.cursorPos < self.runes.len():
396396
self.runes[self.cursorPos] = rune
397397
else:
398398
self.runes.insert(rune, self.cursorPos)
399-
else:
400-
self.runes.insert(rune, self.cursorPos)
399+
# 4. yes overWrite, yes rangeLimit
400+
elif overWrite and rangeLimit > 0:
401+
if self.cursorPos <= rangeLimit - 1:
402+
if self.cursorPos < self.runes.len():
403+
self.runes[self.cursorPos] = rune
404+
else:
405+
self.runes.insert(rune, self.cursorPos)
401406

402407
self.updateLayout()
403408
self.shiftCursor(Right)
404409

405-
proc insert*(self: var TextBox, runes: seq[Rune]) =
410+
proc insert*(self: var TextBox,
411+
runes: seq[Rune],
412+
overWrite = false) =
406413

407-
if self.hasSelection():
414+
if self.selectionExists:
408415
self.deleteSelected()
409416

410-
if Overwrite in self.opts:
417+
if overWrite:
411418
for i in 0..<runes.len():
412419
if self.cursorPos < self.runes.len():
413420
self.runes[self.cursorPos] = runes[i]

src/figuro/widgets/input.nim

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ type
2222
cursorTick: int
2323
cursorCnt: int
2424
skipOnInput*: HashSet[Rune]
25+
rangeLimit*: int = 0
2526

2627
Cursor* = ref object of Figuro
2728
Selection* = ref object of Figuro
2829

2930
proc setOptions*(self: Input, opt: set[InputOptions], state = true) {.thisWrapper.} =
3031
if state: self.opts.incl opt
3132
else: self.opts.excl opt
32-
if OverwriteMode in opt:
33-
self.text.options({TextOptions.Overwrite}, state)
3433

3534
proc skipOnInput*(self: Input, runes: HashSet[Rune]) =
3635
## skips the given runes and advances the cursor to the
@@ -118,7 +117,10 @@ proc keyInput*(self: Input, rune: Rune) {.slot.} =
118117

119118
proc updateInput*(self: Input, rune: Rune) {.slot.} =
120119
self.skipSelectedRune()
121-
self.text.insert(rune)
120+
if OverwriteMode in self.opts:
121+
self.text.insert(rune, overWrite = true, self.rangeLimit)
122+
else:
123+
self.text.insert(rune, overWrite = false, self.rangeLimit)
122124
self.text.update(self.box)
123125
refresh(self)
124126

@@ -144,11 +146,13 @@ proc keyCommand*(self: Input, pressed: set[UiKey], down: set[UiKey]) {.slot.} =
144146
var update = true
145147
case pressed.getKey()
146148
of KeyBackspace:
147-
self.text.delete(Left)
148-
self.text.update(self.box)
149+
if NoErase notin self.opts:
150+
self.text.delete(Left)
151+
self.text.update(self.box)
149152
of KeyDelete:
150-
self.text.delete(Right)
151-
self.text.update(self.box)
153+
if NoErase notin self.opts:
154+
self.text.delete(Right)
155+
self.text.update(self.box)
152156
of KeyLeft:
153157
self.text.shiftCursor(Left)
154158
of KeyRight:

tests/tinput.nim

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ proc draw*(self: Main) {.slot.} =
3737
this.text.shiftCursor(TheEnd)
3838
this.activate()
3939

40-
4140
var main = Main.new()
4241
var frame = newAppFrame(main, size=(720'ui, 140'ui), saveWindowState = false)
4342
startFiguro(frame)

tests/tinput2.nim

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,15 @@ proc draw*(self: Main) {.slot.} =
8282
proc overrideUpdateInput(this: Input, rune: Rune) {.slot.} =
8383
let isDigit = rune <=% Rune('9') and rune.char in {'0'..'9'}
8484
template currCharColon(): bool = this.text.runeAtCursor() == Rune(':')
85+
this.skipSelectedRune()
8586
if isDigit:
86-
this.updateInput(rune)
87+
this.text.insert(rune, overWrite = true, rangeLimit = 8)
8788
onInit:
8889
this.activate()
8990

91+
disconnect(this, doUpdateInput, this, updateInput)
9092
connect(this, doUpdateInput, this, overrideUpdateInput)
93+
9194
if not this.textChanged(""):
9295
# set default
9396
this.setText("00:00:00")

tests/unittests/ttextboxes.nim

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -172,66 +172,53 @@ suite "text boxes (single line)":
172172
check tx.runes == "alpha".toRunes()
173173

174174
test "set text overwrite":
175-
text.opts.incl Overwrite
176-
177175
text.placeCursor(0)
178-
text.insert("o".runeAt(0))
176+
text.insert("o".runeAt(0), overWrite = true)
179177
check text.runes == "obcd".toRunes()
180178
text.placeCursor(0)
181-
text.insert("u".runeAt(0))
179+
text.insert("u".runeAt(0), true)
182180
check text.runes == "ubcd".toRunes()
183181

184182
text.placeCursor(1)
185-
text.insert("x".runeAt(0))
183+
text.insert("x".runeAt(0), true)
186184
check text.runes == "uxcd".toRunes()
187185
text.placeCursor(1)
188-
text.insert("y".runeAt(0))
186+
text.insert("y".runeAt(0), true)
189187
check text.runes == "uycd".toRunes()
190188

189+
test "set text overwrite end, with rangelimit":
190+
text.selection = 4..4
191+
text.insert("x".runeAt(0), true, rangeLimit = 4)
192+
check text.runes == "abcd".toRunes()
191193

192-
# The insert proc needs another option like RangeLimit;
193-
# otherwise, Overwrite can't be used as a general purpose toggle.
194-
# test "set text overwrite end":
195-
# text.opts.incl Overwrite
196-
197-
# text.selection = 4..4
198-
# text.insert("x".runeAt(0))
199-
# check text.runes == "abcd".toRunes()
200-
201-
# text.selection = 3..3
202-
# text.insert("x".runeAt(0))
203-
# check text.runes == "abcx".toRunes()
194+
text.selection = 3..3
195+
text.insert("x".runeAt(0), true, 4)
196+
check text.runes == "abcx".toRunes()
204197

205198
test "set text overwrite selected":
206-
text.opts.incl Overwrite
207199
text.selection = 2..3
208-
text.insert("o".runeAt(0))
200+
text.insert("o".runeAt(0), true)
209201
check text.runes == "abo".toRunes()
210202

211203
test "set text overwrite many selected":
212-
text.opts.incl Overwrite
213204
text.selection = 2..4
214205
text.placeCursor(2)
215-
text.insert("xy".toRunes())
206+
text.insert("xy".toRunes(), true)
216207
check text.runes == "abxy".toRunes()
217208

218209
test "set text overwrite many selected":
219-
text.opts.incl Overwrite
220210
text.selection = 4..4
221-
text.insert("x".toRunes())
211+
text.insert("x".toRunes(), true)
222212
check text.runes == "abcd".toRunes()
223213

224214
test "set text overwrite single":
225-
text.opts.incl Overwrite
226215
text.selection = 0..0
227-
text.insert("x".toRunes())
216+
text.insert("x".toRunes(), true)
228217
check text.runes == "xbcd".toRunes()
229218

230219
test "set text overwrite multiple":
231-
text.opts.incl Overwrite
232-
233220
text.selection = 0..0
234-
text.insert("xy".toRunes())
221+
text.insert("xy".toRunes(), true)
235222
check text.runes == "xycd".toRunes()
236223

237224
test "set text with longer selected text":

0 commit comments

Comments
 (0)