Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Widgets/text area #4995

Merged
merged 22 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8bc9da2
Create TextArea widget, based on `gui/journal` widget
wiktor-obrebski Oct 8, 2024
55ae723
Migrate journal text-area related test to core TestArea test module
wiktor-obrebski Oct 8, 2024
e237387
Add documentation for new TextArea widget
wiktor-obrebski Oct 9, 2024
83a9a19
Add way to set text from the TextArea widget API
wiktor-obrebski Oct 9, 2024
3ce4f1f
Abandon named subviews for TextArea to avoid collisions with user code
wiktor-obrebski Oct 9, 2024
fa68bbe
Improve TextArea docs
wiktor-obrebski Oct 9, 2024
cb549ed
Add tests for TextArea undo feature
wiktor-obrebski Oct 11, 2024
ff41a5e
Add undo/redo textarea widget features tests
wiktor-obrebski Oct 11, 2024
2ed6dcb
Add clear history textarea feature
wiktor-obrebski Oct 11, 2024
58c8d9b
Add history entry (undo/redo) for TextArea API text set (:setText)
wiktor-obrebski Oct 11, 2024
cdda92e
Make TextArea on_cursor_change include old cursor
wiktor-obrebski Nov 17, 2024
a307308
Add docs about how TextArea cursor works
wiktor-obrebski Nov 17, 2024
b9422dc
Add old_text to TextArea widget text change callback
wiktor-obrebski Nov 20, 2024
51ff6b7
Drop now redundant TextArea tests version boundary
wiktor-obrebski Nov 21, 2024
1f75f37
Improve TextArea documentation
wiktor-obrebski Nov 21, 2024
ff76073
Improve TextArea documentation
wiktor-obrebski Nov 21, 2024
6384b21
Improve TextArea focus handling
wiktor-obrebski Nov 21, 2024
6047b40
Polishing TextArea subwidgets
wiktor-obrebski Nov 21, 2024
83f9df3
Remove trailing white space from docs
wiktor-obrebski Nov 22, 2024
4250bb4
Improve TextArea RST documentation structure
wiktor-obrebski Nov 22, 2024
abebac7
Improve TextArea focus
wiktor-obrebski Nov 22, 2024
c005846
Add comment about TextArea rendering
wiktor-obrebski Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions library/lua/gui/widgets.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ FilteredList = require('gui.widgets.filtered_list')
TabBar = require('gui.widgets.tab_bar')
RangeSlider = require('gui.widgets.range_slider')
DimensionsTooltip = require('gui.widgets.dimensions_tooltip')
TextArea = require('gui.widgets.text_area')

Tab = TabBar.Tab
makeButtonLabelText = Label.makeButtonLabelText
Expand Down
166 changes: 166 additions & 0 deletions library/lua/gui/widgets/text_area.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
-- Multiline text area control

local Panel = require('gui.widgets.containers.panel')
local Scrollbar = require('gui.widgets.scrollbar')
local TextAreaContent = require('gui.widgets.text_area.text_area_content')

TextArea = defclass(TextArea, Panel)

TextArea.ATTRS{
init_text = '',
wiktor-obrebski marked this conversation as resolved.
Show resolved Hide resolved
init_cursor = DEFAULT_NIL,
text_pen = COLOR_LIGHTCYAN,
ignore_keys = {'STRING_A096'},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we can take this out and default to an empty list. We seem to have fixed the issue of backtick appearing in text boxes via the INTERCEPT_HANDLED_HOTKEYS feature that I added a while back. Testing shows that we can even remove ignore_keys={'STRING_A096'}, from gui/launcher.

select_pen = COLOR_CYAN,
on_text_change = DEFAULT_NIL,
on_cursor_change = DEFAULT_NIL,
one_line_mode = false,
debug = false
}

function TextArea:init()
self.render_start_line_y = 1

self:addviews{
TextAreaContent{
view_id='text_area',
wiktor-obrebski marked this conversation as resolved.
Show resolved Hide resolved
frame={l=0,r=3,t=0},
text=self.init_text,

text_pen=self.text_pen,
ignore_keys=self.ignore_keys,
select_pen=self.select_pen,
debug=self.debug,
one_line_mode=self.one_line_mode,

on_text_change=function (val)
self:updateLayout()
if self.on_text_change then
self.on_text_change(val)
end
end,
on_cursor_change=self:callback('onCursorChange')
},
Scrollbar{
view_id='scrollbar',
frame={r=0,t=1},
on_scroll=self:callback('onScrollbar'),
visible=not self.one_line_mode
}
}
self:setFocus(true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be handled in the getPreferredFocusState override, otherwise there will be conflicts when there are multiple TextArea widgets in a single focus group

end

function TextArea:getText()
return self.subviews.text_area.text
end

wiktor-obrebski marked this conversation as resolved.
Show resolved Hide resolved
function TextArea:getCursor()
return self.subviews.text_area.cursor
end

function TextArea:onCursorChange(cursor)
local x, y = self.subviews.text_area.wrapped_text:indexToCoords(
self.subviews.text_area.cursor
)

if y >= self.render_start_line_y + self.subviews.text_area.frame_body.height then
self:updateScrollbar(
y - self.subviews.text_area.frame_body.height + 1
)
elseif (y < self.render_start_line_y) then
self:updateScrollbar(y)
end

if self.on_cursor_change then
self.on_cursor_change(cursor)
end
end

function TextArea:scrollToCursor(cursor_offset)
if self.subviews.scrollbar.visible then
local _, cursor_liny_y = self.subviews.text_area.wrapped_text:indexToCoords(
cursor_offset
)
self:updateScrollbar(cursor_liny_y)
end
end

function TextArea:setCursor(cursor_offset)
return self.subviews.text_area:setCursor(cursor_offset)
end

function TextArea:getPreferredFocusState()
return self.parent_view.focus
end

function TextArea:postUpdateLayout()
self:updateScrollbar(self.render_start_line_y)

if self.subviews.text_area.cursor == nil then
local cursor = self.init_cursor or #self.init_text + 1
self.subviews.text_area:setCursor(cursor)
self:scrollToCursor(cursor)
end
end

function TextArea:onScrollbar(scroll_spec)
local height = self.subviews.text_area.frame_body.height

local render_start_line = self.render_start_line_y
if scroll_spec == 'down_large' then
render_start_line = render_start_line + math.ceil(height / 2)
elseif scroll_spec == 'up_large' then
render_start_line = render_start_line - math.ceil(height / 2)
elseif scroll_spec == 'down_small' then
render_start_line = render_start_line + 1
elseif scroll_spec == 'up_small' then
render_start_line = render_start_line - 1
else
render_start_line = tonumber(scroll_spec)
end

self:updateScrollbar(render_start_line)
end

function TextArea:updateScrollbar(scrollbar_current_y)
local lines_count = #self.subviews.text_area.wrapped_text.lines

local render_start_line_y = (math.min(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the outer parentheses are unnecessary and don't add clarity

#self.subviews.text_area.wrapped_text.lines - self.subviews.text_area.frame_body.height + 1,
math.max(1, scrollbar_current_y)
))

self.subviews.scrollbar:update(
render_start_line_y,
self.frame_body.height,
lines_count
)

if (self.frame_body.height >= lines_count) then
render_start_line_y = 1
end

self.render_start_line_y = render_start_line_y
self.subviews.text_area:setRenderStartLineY(self.render_start_line_y)
end

function TextArea:renderSubviews(dc)
self.subviews.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1)

TextArea.super.renderSubviews(self, dc)
end

function TextArea:onInput(keys)
if (self.subviews.scrollbar.is_dragging) then
return self.subviews.scrollbar:onInput(keys)
end

if keys._MOUSE_L and self:getMousePos() then
self:setFocus(true)
end

return TextArea.super.onInput(self, keys)
end

return TextArea
81 changes: 81 additions & 0 deletions library/lua/gui/widgets/text_area/history_store.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
HistoryStore = defclass(HistoryStore)

local HISTORY_ENTRY = {
TEXT_BLOCK = 1,
WHITESPACE_BLOCK = 2,
BACKSPACE = 2,
DELETE = 3,
OTHER = 4
}

HistoryStore.ATTRS{
history_size = 25,
}

function HistoryStore:init()
self.past = {}
self.future = {}
end

function HistoryStore:store(history_entry_type, text, cursor)
local last_entry = self.past[#self.past]

if not last_entry or history_entry_type == HISTORY_ENTRY.OTHER or
last_entry.entry_type ~= history_entry_type then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is beautiful. such a clean way to implement deduplication of successive similar events.

table.insert(self.past, {
entry_type=history_entry_type,
text=text,
cursor=cursor
})
end

self.future = {}

if #self.past > self.history_size then
table.remove(self.past, 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably ok for now, but if larger history sizes are used, we should switch to using ring buffers for self.past and self.future

end
end

function HistoryStore:undo(curr_text, curr_cursor)
if #self.past == 0 then
return nil
end

local history_entry = table.remove(self.past, #self.past)

table.insert(self.future, {
entry_type=HISTORY_ENTRY.OTHER,
text=curr_text,
cursor=curr_cursor
})

if #self.future > self.history_size then
table.remove(self.future, 1)
end

return history_entry
end

function HistoryStore:redo(curr_text, curr_cursor)
if #self.future == 0 then
return true
end

local history_entry = table.remove(self.future, #self.future)

table.insert(self.past, {
entry_type=HISTORY_ENTRY.OTHER,
text=curr_text,
cursor=curr_cursor
})

if #self.past > self.history_size then
table.remove(self.past, 1)
end

return history_entry
end

HistoryStore.HISTORY_ENTRY = HISTORY_ENTRY

return HistoryStore
Loading