From 78547a15015543719e6b89b45e26e6037730cc1a Mon Sep 17 00:00:00 2001 From: Rene Kievits Date: Tue, 21 Mar 2023 16:20:50 +0100 Subject: [PATCH] more work for inputbox --- awesome/src/modules/init.lua | 9 +- awesome/src/modules/inputbox/init.lua | 1186 ++++++++++--------------- awesome/src/modules/inputbox/new.lua | 472 ---------- 3 files changed, 452 insertions(+), 1215 deletions(-) delete mode 100644 awesome/src/modules/inputbox/new.lua diff --git a/awesome/src/modules/init.lua b/awesome/src/modules/init.lua index a15c1c8..76e98b8 100644 --- a/awesome/src/modules/init.lua +++ b/awesome/src/modules/init.lua @@ -17,24 +17,23 @@ awful.screen.connect_for_each_screen(function(s) require('src.modules.application_launcher.init') { screen = s } end) -local ip = require('src.modules.inputbox.new') { +local ip = require('src.modules.inputbox.init') { text = 'inputboxtest', cursor_pos = 4, highlight = { start_pos = 1, end_pos = 4, }, - text_hint = 'Input Some Text', + text_hint = 'Start typing...', } -awful.popup { +--[[ awful.popup { widget = ip.widget, bg = '#212121', visible = true, screen = 1, placement = awful.placement.centered, -} - +} ]] --[[ require('src.modules.inputbox.init') { text = 'inputboxtest', cursor_pos = 4, diff --git a/awesome/src/modules/inputbox/init.lua b/awesome/src/modules/inputbox/init.lua index 18a2604..c1c1e13 100644 --- a/awesome/src/modules/inputbox/init.lua +++ b/awesome/src/modules/inputbox/init.lua @@ -1,27 +1,23 @@ ---------------------------------------------------------------------------- --- This widget can be used to type text and get the text from it. ---@DOC_wibox_widget_defaults_inputbox_EXAMPLE@ --- --- @author Rene Kievits --- @copyright 2022, Rene Kievits --- @module awful.widget.inputbox ---------------------------------------------------------------------------- - -local setmetatable = setmetatable -local beautiful = require('beautiful') -local gtable = require('gears.table') -local base = require('wibox.widget.base') -local gstring = require('gears.string') -local akeygrabber = require('awful.keygrabber') -local akey = require('awful.key') -local textbox = require('wibox.widget.textbox') -local imagebox = require('wibox.widget.imagebox') -local cairo = require('lgi').cairo -local apopup = require('awful.popup') -local aplacement = require('awful.placement') -local gsurface = require('gears.surface') -local wibox = require('wibox') +local Pango = require('lgi').Pango +local PangoCairo = require('lgi').PangoCairo local abutton = require('awful.button') +local akey = require('awful.key') +local akeygrabber = require('awful.keygrabber') +local aplacement = require('awful.placement') +local apopup = require('awful.popup') +local base = require('wibox.widget.base') +local beautiful = require('beautiful') +local cairo = require('lgi').cairo +local gobject = require('gears.object') +local gstring = require('gears.string') +local gsurface = require('gears.surface') +local gtable = require('gears.table') +local gtimer = require('gears.timer') +local imagebox = require('wibox.widget.imagebox') +local setmetatable = setmetatable +local textbox = require('wibox.widget.textbox') +local wibox = require('wibox') +local dpi = beautiful.xresources.apply_dpi local capi = { selection = selection, @@ -29,768 +25,482 @@ local capi = { mouse = mouse, } -local inputbox = { mt = {} } +local inputbox = {} ---- Formats the text with a cursor and highlights if set. ---[[ local function text_with_cursor(text, cursor_pos, self) - local char, spacer, text_start, text_end +local function get_subtext_layout(layout, starti, endi) + local text = layout:get_text() - local cursor_fg = beautiful.inputbox_cursor_fg or '#313131' - local cursor_bg = beautiful.inputbox_cursor_bg or '#0dccfc' - local placeholder_text = self.hint_text or '' - local placeholder_fg = beautiful.inputbox_placeholder_fg or '#777777' - local highlight_bg = beautiful.inputbox_highlight_bg or '#35ffe4' - local highlight_fg = beautiful.inputbox_highlight_fg or '#000000' + local subtext = text:sub(starti, endi) - if text == '' then - return "" .. placeholder_text .. '' - end + local ctx = layout:get_context() - local offset = 0 - if text:sub(cursor_pos - 1, cursor_pos - 1) == -1 then - offset = 1 - end + local sublayout = Pango.Layout.new(ctx) + sublayout:set_font_description(layout:get_font_description()) + sublayout:set_text(subtext) - if #text < cursor_pos then - char = ' ' - spacer = '' - text_start = gstring.xml_escape(text) - text_end = '' - else - char = gstring.xml_escape(text:sub(cursor_pos, cursor_pos + offset)) - spacer = ' ' - text_start = gstring.xml_escape(text:sub(1, cursor_pos - 1)) - text_end = gstring.xml_escape(text:sub(cursor_pos + offset + 1)) - end - - if self._private.highlight and self._private.highlight.start_pos and self._private.highlight.end_pos then - -- split the text into 3 parts based on the highlight and cursor position - local text_start_highlight = gstring.xml_escape(text:sub(1, self._private.highlight.start_pos - 1)) - local text_highlighted = gstring.xml_escape(text:sub(self._private.highlight.start_pos, - self._private.highlight.end_pos)) - local text_end_highlight = gstring.xml_escape(text:sub(self._private.highlight.end_pos + 1)) - - return text_start_highlight .. - "" .. - text_highlighted .. '' .. text_end_highlight - else - return text_start .. "" .. - char .. '' .. text_end .. spacer - end -end ]] - -local function text_extents(text, font, font_size, args) - local surface = cairo.ImageSurface(cairo.Format.ARGB32, 0, 0) - local cr = cairo.Context(surface) - cr:select_font_face(font, args) - cr:set_font_size(font_size) - return cr:text_extents(text) + local _, sub_extent = sublayout:get_extents() + return sub_extent end ---[[ - calculate width/height of the text - create new surface with the calculated width/height - draw a vertical line on the surface as the cursor - the position of the vertical line will be the cursor_pos - text length and the text extend - draw the "text" .. "cursor" .. "rest of the text" - return the surface +function inputbox.draw_text(self) + local text = self:get_text() + local highlight = self:get_highlight() + local fg_color = { 1, 1, 1, 1 } + local cursor_color = { 1, 1, 1, 1 } - mouse_coord holds the coordinates where the user clicked - if its not empty then draw the cursor where the user clicked, - if its on a character then set the cursor to the closest space + if text == '' then + fg_color = { 0.2, 0.2, 0.2, 1 } + --self._private.layout:set_text(self._private.text_hint) + --local cairo_text = self._private.text_hint + -- Get the text extents from Pango so we don't need to use cairo to get a possibly wrong extent + -- Then draw the text with cairo + end - if some text is highlighted then draw the text with the highlights -]] ---inputbox.text ---inputbox.cursor_pos <<-- 1 = before the text, 2 = after the first character, 3 = after the second character, etc ---inputbox.highlight <<-- { start_pos, end_pos } ---inputbox.mouse_coord <<-- { x, y } (Will be saved after the user clicked, it will only be overwritten when the user clicks again) -function inputbox:draw_text_surface(x, override_cursor) - -- x can be 0 for the first time its drawn with a default cursor position - x = x or 0 + local _, pango_extent = self._private.layout:get_extents() - --Colors need to be in rgba 0-1 format, table.unpack is used to unpack the table into function arguments - local fg, fg_highlight, bg_highlight, fg_cursor = { 1, 1, 1 }, { 1, 1, 1 }, { 0.1, 1, 1, 0.5 }, { 1, 1, 1 } - - -- Main text_entent mainly to align everything to this one (it knows the highest and lowest point of the text) - local text_extent = text_extents(self:get_text(), self.font, self.font_size, { cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL }) - - --The offset if so the user has some space to click on the left and right side of the text - local start_offset = 4 - local end_offset = 4 - local surface = cairo.ImageSurface(cairo.Format.ARGB32, text_extent.width + start_offset + end_offset, text_extent.height) + local surface = cairo.ImageSurface(cairo.Format.ARGB32, (pango_extent.width / Pango.SCALE) + pango_extent.x + 2, (pango_extent.height / Pango.SCALE) + pango_extent.y) local cr = cairo.Context(surface) - --Split the text initially into 2 or 3 parts (most likely split again later) - local text = self:get_text() - local text_start, text_end, text_highlight - if self._private.highlight.start_pos ~= self._private.highlight.end_pos then - text_start = text:sub(1, self._private.highlight.start_pos - 1) - text_highlight = text:sub(self._private.highlight.start_pos, self._private.highlight.end_pos) - text_end = text:sub(self._private.highlight.end_pos + 1) + -- Draw highlight + if highlight.start_pos ~= highlight.end_pos then + cr:set_source_rgb(0, 0, 1) + local sub_extent = get_subtext_layout(self._private.layout, self:get_highlight().start_pos, self:get_highlight().end_pos) + local _, x_offset = self._private.layout:index_to_line_x(self:get_highlight().start_pos, false) + cr:rectangle( + x_offset / Pango.SCALE, + pango_extent.y / Pango.SCALE, + sub_extent.width / Pango.SCALE, + pango_extent.height / Pango.SCALE + ) + cr:fill() + end + + -- Draw text + if not self.password_mode then + PangoCairo.update_layout(cr, self._private.layout) + cr:set_source_rgba(table.unpack(fg_color)) + cr:move_to(0, 0) + PangoCairo.show_layout(cr, self._private.layout) else - text_start = text:sub(1, self._private.cursor_pos - 1) - text_end = text:sub(self._private.cursor_pos) - end - - --Figure out the cursor position based on the mouse coordinates - if override_cursor then - local cursor_pos = 1 - for i = 1, #text, 1 do - -- Not sure if I need new context's to check the character width but I got inconsistent results without it - local ccr = cairo.Context(surface) - ccr:select_font_face(self.font, { cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL }) - ccr:set_font_size(self.font_size) - local ext_c = ccr:text_extents(text:sub(1, i)) - if ext_c.width >= x then - local cccr = cairo.Context(surface) - cccr:select_font_face(self.font, { cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL }) - cccr:set_font_size(self.font_size) - if math.abs(cccr:text_extents(text:sub(1, i - 1)).width - x) <= math.abs(ext_c.width - x) then - cursor_pos = i - else - cursor_pos = i + 1 - end - break - else - cursor_pos = #text + 1 - end + local count = self._private.layout:get_glyph_count() + local passwd_string + for i = 1, count, 1 do + passwd_string = passwd_string .. '🞄' end - self._private.cursor_pos = cursor_pos + self._private.layout:set_text(passwd_string) + PangoCairo.update_layout(cr, self._private.layout) + cr:set_source_rgba(table.unpack(fg_color)) + cr:move_to(0, 0) + PangoCairo.show_layout(cr, self._private.layout) end - -- Text extents for the start and highlight without any splitting (less calculating, text_end is not needed) - local text_start_extents = text_extents(text_start, self.font, self.font_size, { cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL }) - local text_highlight_extents = text_extents(text_highlight, self.font, self.font_size, { cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL }) + -- Draw cursor + cr:set_source_rgba(table.unpack(cursor_color)) + local cursor = self:get_cursor_pos() + cr:rectangle( + cursor.x / Pango.SCALE, + cursor.y / Pango.SCALE, + 2, + cursor.height / Pango.SCALE) + cr:fill() - cr:select_font_face(self.font, cairo.FontSlant.NORMAL, cairo.FontWeight.REGULAR) - cr:set_font_size(self.font_size) - --[[ - The following code is a bit of a mess because I have to check if the cursor is inside the highlighted text, - the text_start or text_end and then split either of them again to draw the cursor between. - ]] - if (self._private.cursor_pos > 1) and text_highlight then - -- If the cursor is inside the highlighted text - if (self._private.highlight.start_pos <= self._private.cursor_pos) and (self._private.highlight.end_pos >= self._private.cursor_pos) then - -- Draw the text_start - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset, -text_extent.y_bearing) - cr:show_text(text_start) - - -- split the text_highlight at the cursor_pos - local text_highlight_start = text_highlight:sub(1, self._private.cursor_pos - self._private.highlight.start_pos) - local text_highlight_end = text_highlight:sub(self._private.cursor_pos - self._private.highlight.start_pos + 1) - -- The text_highlight_start extents are needed for the cursor position and the text_highlight_end position - local text_highlight_start_extents = text_extents(text_highlight_start, self.font, self.font_size, { cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL }) - - -- Draw the first highlighted part(text_highlight_start) - cr:set_source_rgb(table.unpack(fg_highlight)) - cr:move_to(start_offset + text_start_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_highlight_start) - - -- Draw the cursor - cr:set_source_rgb(table.unpack(fg_cursor)) - cr:move_to(start_offset + text_start_extents.x_advance + text_highlight_start_extents.x_advance, text_extent.y_bearing) - cr:line_to(start_offset + text_start_extents.x_advance + text_highlight_start_extents.x_advance, text_extent.height) - cr:stroke() - - -- Draw the second highlighted part(text_highlight_end) - cr:set_source_rgb(table.unpack(fg_highlight)) - cr:move_to(start_offset + text_start_extents.x_advance + text_highlight_start_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_highlight_end) - - -- Draw the text_end - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset + text_start_extents.x_advance + text_highlight_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_end) - - -- Draw the background highlight - cr:set_source_rgba(table.unpack(bg_highlight)) - cr:rectangle(start_offset + text_start_extents.x_advance, text_extent.y_advance, text_highlight_extents.width, text_extent.height) - cr:fill() - elseif self._private.cursor_pos < self._private.highlight.start_pos then -- If its inside the text_start - -- Split the text_start at the cursor_pos - local text_start_start = text_start:sub(1, self._private.cursor_pos - 1) - local text_start_end = text_start:sub(self._private.cursor_pos) - -- The text_start_start extents is needed for the cursor position and the text_start_end position - local text_start_start_extents = text_extents(text_start_start, self.font, self.font_size, { cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL }) - - -- Draw the first part of the text_start(text_start_start) - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset, -text_extent.y_bearing) - cr:show_text(text_start_start) - - -- Draw the cursor - cr:set_source_rgb(table.unpack(fg_cursor)) - cr:move_to(start_offset + text_start_start_extents.x_advance, text_extent.y_bearing) - cr:line_to(start_offset + text_start_start_extents.x_advance, text_extent.height) - cr:stroke() - - -- Draw the second part of the text_start(text_start_end) - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset + text_start_start_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_start_end) - - -- Draw the text_highlight - cr:set_source_rgb(table.unpack(fg_highlight)) - cr:move_to(start_offset + text_start_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_highlight) - - -- Draw the text_end - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset + text_start_extents.x_advance + text_highlight_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_end) - - -- Draw the highlight background - cr:set_source_rgba(table.unpack(bg_highlight)) - cr:rectangle(start_offset + text_start_extents.x_advance, text_extent.y_advance, text_highlight_extents.width, text_extent.height) - cr:fill() - elseif self._private.cursor_pos > self._private.highlight.end_pos then -- If its inside the text_end - -- Draw the text start - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset, -text_extent.y_bearing) - cr:show_text(text_start) - - -- Draw the text highlight - cr:set_source_rgb(table.unpack(fg_highlight)) - cr:move_to(start_offset + text_start_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_highlight) - - --split the text_end at the cursor_pos - local text_end_start = text_end:sub(1, self._private.cursor_pos - self._private.highlight.end_pos - 1) - local text_end_end = text_end:sub(self._private.cursor_pos - self._private.highlight.end_pos) - -- Text end_start extents needed for the cursor position and the text_end_end - local text_end_start_extents = text_extents(text_end_start, self.font, self.font_size, { cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL }) - - -- Draw the first part of the text_end (text_end_start) - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset + text_start_extents.x_advance + text_highlight_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_end_start) - - -- Draw the cursor - cr:set_source_rgb(table.unpack(fg_cursor)) - cr:move_to(start_offset + text_start_extents.x_advance + text_highlight_extents.x_advance + text_end_start_extents.x_advance, text_extent.y_bearing) - cr:line_to(start_offset + text_start_extents.x_advance + text_highlight_extents.x_advance + text_end_start_extents.x_advance, text_extent.height) - cr:stroke() - - -- Draw the second part of the text_end (text_end_end) - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset + text_start_extents.x_advance + text_highlight_extents.x_advance + text_end_start_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_end_end) - - -- Draw the highlight background - cr:set_source_rgba(table.unpack(bg_highlight)) - cr:rectangle(start_offset + text_start_extents.x_advance, text_extent.y_advance, text_highlight_extents.width, text_extent.height) - cr:fill() - end - else -- If the cursor is all the way to the left no split is needed - -- text_start - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset, -text_extent.y_bearing) - cr:show_text(text_start) - - -- Cursor - cr:set_source_rgb(table.unpack(fg_cursor)) - cr:move_to(start_offset + text_start_extents.x_advance, text_extent.y_bearing) - cr:line_to(start_offset + text_start_extents.x_advance, text_extent.height) - cr:stroke() - - -- text_highlight - if text_highlight then - cr:set_source_rgb(table.unpack(fg_highlight)) - cr:move_to(start_offset + text_start_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_highlight) - cr:set_source_rgba(table.unpack(bg_highlight)) - cr:rectangle(start_offset + text_start_extents.x_advance, text_extent.y_advance, text_highlight_extents.width, text_extent.height) - cr:fill() - end - - -- text_end - cr:set_source_rgb(table.unpack(fg)) - cr:move_to(start_offset + text_highlight_extents.x_advance + text_start_extents.x_advance, -text_extent.y_bearing) - cr:show_text(text_end) - end + self.widget:set_image(surface) return surface end -function inputbox:layout(_, width, height) - if self._private.widget then - return { base.place_widget_at(self._private.widget, 0, 0, width, height) } +function inputbox:start_keygrabber() + self._private.akeygrabber = akeygrabber { + autostart = true, + stop_key = { 'Escape', 'Return' }, + start_callback = function() + end, + stop_callback = function() + end, + keybindings = { + akey { -- Delete highlight or left to cursor + modifiers = {}, + key = 'BackSpace', + on_press = function() + local hl = self:get_highlight() + local text = self:get_text() + local cursor_pos = self:get_cursor_index() + if hl.end_pos ~= hl.start_pos then + self:set_text(text:sub(0, hl.start_pos) .. text:sub(hl.end_pos + 1, #text)) + self:set_cursor_pos(hl.start_pos) + self:set_highlight { start_pos = 0, end_pos = 0 } + else + self:set_text(text:sub(1, cursor_pos - 1) .. text:sub(cursor_pos + 1)) + self:set_cursor_pos(cursor_pos - 1) + end + end, + }, + akey { -- Delete highlight or right of cursor + modifiers = {}, + key = 'Delete', + on_press = function() + local hl = self:get_highlight() + local text = self:get_text() + local cursor_pos = self:get_cursor_index() + if hl.end_pos ~= hl.start_pos then + self:set_text(text:sub(0, hl.start_pos) .. text:sub(hl.end_pos + 1, #text)) + self:set_cursor_pos(hl.start_pos) + self:set_highlight { start_pos = 0, end_pos = 0 } + else + self:set_text(text:sub(1, cursor_pos) .. text:sub(cursor_pos + 2, #text)) + end + end, + }, + akey { -- Move cursor to left + modifiers = {}, + key = 'Left', + on_press = function() + self:set_cursor_pos(self:get_cursor_index() - 1) + self:set_highlight { start_pos = 0, end_pos = 0 } + end, + }, + akey { -- Move cursor to right + modifiers = {}, + key = 'Right', + on_press = function() + self:set_cursor_pos(self:get_cursor_index() + 1) + self:set_highlight { start_pos = 0, end_pos = 0 } + end, + }, + akey { -- Jump cursor to text beginning + modifiers = {}, + key = 'Home', + on_press = function() + self:set_cursor_pos(0) + self:set_highlight { start_pos = 0, end_pos = 0 } + end, + }, + akey { -- Jump cursor to text end + modifiers = {}, + key = 'End', + on_press = function() + self:set_cursor_pos(#self:get_text()) + self:set_highlight { start_pos = 0, end_pos = 0 } + end, + }, + akey { -- Highlight to the left + modifiers = { 'Shift' }, + key = 'Left', + on_press = function() + local cursor_pos = self:get_cursor_index() + local hl = self:get_highlight() + if cursor_pos == hl.start_pos then + self:set_cursor_pos(cursor_pos - 1) + self:set_highlight { start_pos = self:get_cursor_index(), end_pos = hl.end_pos } + elseif cursor_pos == hl.end_pos then + self:set_cursor_pos(cursor_pos - 1) + self:set_highlight { start_pos = hl.start_pos, end_pos = self:get_cursor_index() } + else + if (hl.start_pos ~= cursor_pos) and (hl.end_pos ~= cursor_pos) then + self:set_highlight { start_pos = cursor_pos, end_pos = cursor_pos } + hl = self:get_highlight() + self:set_cursor_pos(cursor_pos - 1) + self:set_highlight { start_pos = self:get_cursor_index(), end_pos = hl.end_pos } + end + end + end, + }, + akey { -- Highlight to the right + modifiers = { 'Shift' }, + key = 'Right', + on_press = function() + local cursor_pos = self:get_cursor_index() + local hl = self:get_highlight() + if cursor_pos == hl.end_pos then + self:set_cursor_pos(cursor_pos + 1) + self:set_highlight { start_pos = hl.start_pos, end_pos = self:get_cursor_index() } + elseif cursor_pos == hl.start_pos then + self:set_cursor_pos(cursor_pos + 1) + self:set_highlight { start_pos = self:get_cursor_index(), end_pos = hl.end_pos } + else + if (hl.start_pos ~= cursor_pos) and (hl.end_pos ~= cursor_pos) then + self:set_highlight { start_pos = cursor_pos, end_pos = cursor_pos } + hl = self:get_highlight() + self:set_cursor_pos(cursor_pos + 1) + self:set_highlight { start_pos = hl.start_pos, end_pos = self:get_cursor_index() } + end + end + end, + }, + akey { -- Highlight all + modifiers = { 'Control' }, + key = 'a', + on_press = function() + self:set_highlight { start_pos = 0, end_pos = #self:get_text() } + self:set_cursor_pos(#self:get_text() - 1) + end, + }, + akey { -- Copy highlight + modifiers = { 'Control' }, + key = 'c', + on_press = function() + local hl = self:get_highlight() + if hl.start_pos ~= hl.end_pos then + local text = self:get_text():sub(hl.start_pos, hl.end_pos) + --TODO:self:copy_to_clipboard(text) + end + end, + }, + akey { -- Paste text into cursor/highlight + modifiers = { 'Control' }, + key = 'v', + on_press = function() + local hl = self:get_highlight() + local selection = capi.selection() + if hl.start_pos ~= hl.end_pos then + self:set_text(self:get_text():sub(1, hl.start_pos) .. selection .. self:get_text():sub(hl.end_pos + 1, #self:get_text())) + self:set_cursor_pos(hl.start_pos + #selection) + self:set_highlight { start_pos = 0, end_pos = 0 } + else + self:set_text(self:get_text():sub(1, self:get_cursor_pos()) .. selection .. self:get_text():sub(self:get_cursor_pos() + 1, #self:get_text())) + self:set_cursor_pos(self:get_cursor_pos() + #selection) + end + end, + }, + akey { -- Cut highlighted text + modifiers = { 'Control' }, + key = 'x', + on_press = function() + --TODO + end, + }, + akey { -- Word jump right + modifiers = { 'Control' }, + key = 'Right', + on_press = function() + + end, + }, + akey { -- Word jump left + modifiers = { 'Control' }, + key = 'Left', + on_press = function() + + end, + }, + akey { -- Word jump highlight right + modifiers = { 'Control', 'Shift' }, + key = 'Right', + on_press = function() + + end, + }, + akey { -- Word jump highlight left + modifiers = { 'Control', 'Shift' }, + key = 'Left', + on_press = function() + + end, + }, + }, + keypressed_callback = function(_, mod, key) + local text = self:get_text() + local cursor_pos = self:get_cursor_index() + if (mod[1] == 'Mod2' or '') and (key:wlen() == 1) then + self:set_text(text:sub(1, cursor_pos) .. key .. text:sub(cursor_pos + 1, #text)) + self:set_cursor_pos(cursor_pos + #key) + elseif (mod[1] == 'Shift') and (key:wlen() == 1) then + self:set_text(text:sub(1, cursor_pos) .. key:upper() .. text:sub(cursor_pos + 1, #text)) + self:set_cursor_pos(cursor_pos + #key) + end + self:emit_signal('inputbox::keypressed', mod, key) + end, + } +end + +function inputbox:start_mousegrabber(x, y) + --[[ if not mousegrabber.isrunning() then + local last_x, advanced, cursor_pos, mx = x, nil, self:get_cursor_pos(), nil + local hl = self:get_highlight() + self:set_highlight { start_pos = cursor_pos, end_pos = cursor_pos } + local text = self:get_text() + mousegrabber.run(function(m) + mx = m.x + if (math.abs(mx - x) > 5) then + advanced, last_x = self:advanced_by_character(mx, last_x) + if advanced == 1 then + if cursor_pos <= hl.start_pos then + if hl.end_pos < #text then + self:set_highlight { start_pos = hl.start_pos, end_pos = hl.end_pos + 1 } + end + else + self:set_highlight { start_pos = hl.start_pos + 1, end_pos = hl.end_pos } + end + elseif advanced == -1 then + if cursor_pos >= hl.end_pos then + if hl.start_pos > 1 then + self:set_highlight { start_pos = hl.start_pos - 1, end_pos = hl.end_pos } + end + else + self:set_highlight { start_pos = hl.start_pos, end_pos = hl.end_pos - 1 } + end + end + end + hl = self:get_highlight() + return m.buttons[1] + end, 'xterm') + end ]] + if not mousegrabber.isrunning() then + local index, _ = self._private.layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE) + + if not index then index = #self:get_text() end + + self:set_cursor_pos(index) + -- Remove highlight, but also prepare its position (same pos = no highlight) + self:set_highlight { start_pos = index, end_pos = index } + + local text = self:get_text() + local hl = self:get_highlight() + + local mb_start = index + local m_start_x = capi.mouse.coords().x + + mousegrabber.run(function(m) + index, _ = self._private.layout:xy_to_index((m.x - m_start_x + x) * Pango.SCALE, y * Pango.SCALE) + + if not index then index = #text end + + if mb_start - index == 1 then + if index <= hl.start_pos then + if hl.end_pos < #text then + self:set_highlight { start_pos = hl.start_pos, end_pos = hl.end_pos + 1 } + end + else + self:set_highlight { start_pos = hl.start_pos + 1, end_pos = hl.end_pos } + end + elseif mb_start - index == -1 then + if index >= hl.end_pos then + if hl.start_pos > 1 then + self:set_highlight { start_pos = hl.start_pos - 1, end_pos = hl.end_pos } + end + else + self:set_highlight { start_pos = hl.start_pos, end_pos = hl.end_pos - 1 } + end + end + + if index ~= mb_start then + self:set_cursor_pos(index) + mb_start = index + end + print(self:get_highlight().start_pos, self:get_highlight().end_pos) + hl = self:get_highlight() + return m.buttons[1] + end, 'xterm') end end -function inputbox:fit(context, width, height) - local w, h = 0, 0 - if self._private.widget then - w, h = base.fit_widget(self, context, self._private.widget, width, height) - end - return w, h -end +function inputbox:set_cursor_pos_from_mouse(x, y) + -- When setting the cursor position, trailing is not needed as its handled by the setter + local index = self._private.layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE) + if not index then index = #self:get_text() end -inputbox.set_widget = base.set_widget_common - ---- Clears the current text -function inputbox:clear() - self:set_text('') + self:set_highlight { start_pos = 0, end_pos = 0 } + self:set_cursor_pos(index) end function inputbox:get_text() - return self._private.text or '' + return self._private.layout:get_text() end function inputbox:set_text(text) - self._private.text = text - --self.markup = text_with_cursor(self:get_text(), #self:get_text(), self) - self:emit_signal('property::text', text) + if self:get_text() == text then return end + + local attributes, parsed = Pango.parse_markup(text, -1, 0) + + if not attributes then return parsed.message or tostring(parsed) end + + self._private.layout:set_text(parsed, string.len(parsed)) + self._private.layout:set_attributes(attributes) + + self.draw_text(self) end ---- Stop the keygrabber and mousegrabber -function inputbox:stop() - if (not self.akeygrabber) or (not self.akeygrabber.is_running) then return end - self:emit_signal('stopped') - self.akeygrabber.stop() +function inputbox:get_cursor_pos() + return self._private.layout:get_cursor_pos(self._private.cursor_pos.index) +end + +function inputbox:get_cursor_index() + return self._private.cursor_pos.index +end + +function inputbox:set_cursor_pos(cursor_pos) + -- moving only moved one character set, to move it multiple times we need to loop as long as the difference to the new cursor isn't 0 + if not cursor_pos or (cursor_pos < 0) or (cursor_pos > #self:get_text()) then return end + --while (cursor_pos - self._private.cursor_pos.index) ~= 0 do + --[[ self._private.cursor_pos.index, self._private.cursor_pos.trailing = self._private.layout:move_cursor_visually( + true, self._private.cursor_pos.index, + self._private.cursor_pos.trailing, + cursor_pos - self._private.cursor_pos.index + ) ]] + self._private.cursor_pos.index = cursor_pos + --end + + self.draw_text(self) +end + +function inputbox:get_highlight() + return self._private.highlight +end + +function inputbox:set_highlight(highlight) + self._private.highlight = highlight + self.draw_text(self) end function inputbox:focus() - if (not self.akeygrabber) or (not self.akeygrabber.is_running) then - akeygrabber.stop() - self:run() - end - - self:connect_signal('button::press', function() - if capi.mouse.current_widget ~= self then - self:emit_signal('keygrabber::stop', '') - end - end) + self:start_keygrabber() end ---- Init the inputbox and start the keygrabber -function inputbox:run() - if not self._private.text then self._private.text = '' end - - -- Init the cursor position, but causes on refocus the cursor to move to the left - local cursor_pos = self._private.cursor_pos or #self:get_text() + 1 - - -- Init and reset(when refocused) the highlight - self._private.highlight = {} - - self.akeygrabber = akeygrabber { - autostart = true, - start_callback = function() - self:emit_signal('started') - end, - stop_callback = function(_, stop_key) - if stop_key == 'Return' then - self:emit_signal('submit', self:get_text(), stop_key) - else - self:emit_signal('stopped', stop_key) - end - end, - stop_key = { 'Escape', 'Return' }, - keybindings = { - --lShift, rShift = #50, #62 - --lControl, rControl = #37, #105 - akey { - modifiers = { 'Shift' }, - key = 'Left', -- left - on_press = function() - if cursor_pos > 1 then - local offset = (self._private.text:sub(cursor_pos - 1, cursor_pos - 1):wlen() == -1) and 1 or 0 - if not self._private.highlight.start_pos then - self._private.highlight.start_pos = cursor_pos - 1 - end - if not self._private.highlight.end_pos then - self._private.highlight.end_pos = cursor_pos - end - - if self._private.highlight.start_pos < cursor_pos then - self._private.highlight.end_pos = self._private.highlight.end_pos - 1 - else - self._private.highlight.start_pos = self._private.highlight.start_pos - end - - cursor_pos = cursor_pos - 1 - end - if cursor_pos < 1 then - cursor_pos = 1 - elseif cursor_pos > #self._private.text + 1 then - cursor_pos = #self._private.text + 1 - end - self._private.cursor_pos = cursor_pos - self.p.widget:get_children_by_id('text_image')[1].image = self:draw_text_surface() - self:emit_signal('inputbox::key_pressed', 'Shift', 'Left') - end, - }, - akey { - modifiers = { 'Shift' }, - key = 'Right', -- right - on_press = function() - if #self._private.text >= cursor_pos then - if not self._private.highlight.end_pos then - self._private.highlight.end_pos = cursor_pos - 1 - end - if not self._private.highlight.start_pos then - self._private.highlight.start_pos = cursor_pos - end - - if self._private.highlight.end_pos <= cursor_pos then - self._private.highlight.end_pos = self._private.highlight.end_pos + 1 - else - self._private.highlight.start_pos = self._private.highlight.start_pos + 1 - end - cursor_pos = cursor_pos + 1 - if cursor_pos > #self._private.text + 1 then - self._private.highlight = {} - end - end - if cursor_pos < 1 then - cursor_pos = 1 - elseif cursor_pos > #self._private.text + 1 then - cursor_pos = #self._private.text + 1 - end - self._private.cursor_pos = cursor_pos - self.p.widget:get_children_by_id('text_image')[1].image = self:draw_text_surface() - self:emit_signal('inputbox::key_pressed', 'Shift', 'Right') - end, - }, - akey { - modifiers = { 'Control' }, - key = 'a', -- a - on_press = function() - -- Mark the entire text - self._private.highlight = { - start_pos = 1, - end_pos = #self._private.text, - } - self.p.widget:get_children_by_id('text_image')[1].image = self:draw_text_surface() - self:emit_signal('inputbox::key_pressed', 'Control', 'a') - end, - }, - akey { - modifiers = { 'Control' }, - key = 'v', -- v - on_press = function() - local sel = capi.selection() - if sel then - sel = sel:gsub('\n', '') - if self._private.highlight and self._private.highlight.start_pos and - self._private.highlight.end_pos then - -- insert the text into the selected part - local text_start = self._private.text:sub(1, self._private.highlight.start_pos - 1) - local text_end = self._private.text:sub(self._private.highlight.end_pos + 1) - self:set_text(text_start .. sel .. text_end) - self._private.highlight = {} - cursor_pos = #text_start + #sel + 1 - else - self:set_text(self._private.text:sub(1, cursor_pos - 1) .. - sel .. self._private.text:sub(cursor_pos)) - cursor_pos = cursor_pos + #sel - end - end - - self.p.widget:get_children_by_id('text_image')[1].image = self:draw_text_surface() - self:emit_signal('inputbox::key_pressed', 'Control', 'v') - end, - }, - akey { - modifiers = { 'Control' }, - key = 'c', -- c - on_press = function() - --TODO - end, - }, - akey { - modifiers = { 'Control' }, - key = 'x', -- x - on_press = function() - --TODO - end, - }, - akey { - modifiers = { 'Control' }, - key = 'Left', -- left - on_press = function() - -- Find all spaces - local spaces = {} - local t, i = self._private.text, 0 - - while t:find('%s') do - i = t:find('%s') - table.insert(spaces, i) - t = t:sub(1, i - 1) .. '-' .. t:sub(i + 1) - end - - local cp = 1 - for _, v in ipairs(spaces) do - if (v < cursor_pos) then - cp = v - end - end - cursor_pos = cp - if cursor_pos < 1 then - cursor_pos = 1 - elseif cursor_pos > #self._private.text + 1 then - cursor_pos = #self._private.text + 1 - end - self.p.widget:get_children_by_id('text_image')[1].image = self:draw_text_surface() - self:emit_signal('inputbox::key_pressed', 'Control', 'Left') - end, - }, - akey { - modifiers = { 'Control' }, - key = 'Right', -- right - on_press = function() - local next_space = self._private.text:sub(cursor_pos):find('%s') - if next_space then - cursor_pos = cursor_pos + next_space - else - cursor_pos = #self._private.text + 1 - end - - if cursor_pos < 1 then - cursor_pos = 1 - elseif cursor_pos > #self._private.text + 1 then - cursor_pos = #self._private.text + 1 - end - self.p.widget:get_children_by_id('text_image')[1].image = self:draw_text_surface() - self:emit_signal('inputbox::key_pressed', 'Control', 'Right') - end, - }, - akey { - modifiers = {}, - key = 'BackSpace', --BackSpace - on_press = function() - -- If text is highlighted delete that, else just delete the character to the left - if self._private.highlight and self._private.highlight.start_pos and - self._private.highlight.end_pos then - local text_start = self._private.text:sub(1, self._private.highlight.start_pos - 1) - local text_end = self._private.text:sub(self._private.highlight.end_pos + 1) - self:set_text(text_start .. text_end) - self._private.highlight = {} - cursor_pos = #text_start + 1 - else - if cursor_pos > 1 then - local offset = (self._private.text:sub(cursor_pos - 1, cursor_pos - 1):wlen() == -1) and 1 or - 0 - self:set_text(self._private.text:sub(1, cursor_pos - 2 - offset) .. - self._private.text:sub(cursor_pos)) - cursor_pos = cursor_pos - 1 - offset - end - end - self.p.widget:get_children_by_id('text_image')[1].image = self:draw_text_surface() - self:emit_signal('inputbox::key_pressed', nil, 'BackSpace') - end, - }, - akey { - modifiers = {}, - key = 'Delete', --delete - on_press = function() - -- If text is highlighted delete that, else just delete the character to the right - if self._private.highlight and self._private.highlight.start_pos and - self._private.highlight.end_pos then - local text_start = self._private.text:sub(1, self._private.highlight.start_pos - 1) - local text_end = self._private.text:sub(self._private.highlight.end_pos + 1) - self:set_text(text_start .. text_end) - self._private.highlight = {} - cursor_pos = #text_start + 1 - else - if cursor_pos <= #self._private.text then - self:set_text(self._private.text:sub(1, cursor_pos - 1) .. - self._private.text:sub(cursor_pos + 1)) - end - end - self.p.widget:get_children_by_id('text_image')[1].image = self:draw_text_surface() - self:emit_signal('inputbox::key_pressed', nil, 'Delete') - end, - }, - akey { - modifiers = {}, - key = 'Left', --left - on_press = function() - -- Move cursor ro the left - if cursor_pos > 1 then - cursor_pos = cursor_pos - 1 - end - self._private.highlight = {} - end, - }, - akey { - modifiers = {}, - key = 'Right', --right - on_press = function() - -- Move cursor to the right - if cursor_pos <= #self._private.text then - cursor_pos = cursor_pos + 1 - end - self._private.highlight = {} - end, - }, - --self.keybindings - }, - keypressed_callback = function(_, modifiers, key) - if modifiers[1] == 'Shift' then - if key:wlen() == 1 then - self:set_text(self._private.text:sub(1, cursor_pos - 1) .. - string.upper(key) .. self._private.text:sub(cursor_pos)) - cursor_pos = cursor_pos + #key - end - elseif modifiers[1] == 'Mod2' or '' then - if key:wlen() == 1 then - self:set_text(self._private.text:sub(1, cursor_pos - 1) .. - key .. self._private.text:sub(cursor_pos)) - cursor_pos = cursor_pos + #key - end - end - - if cursor_pos < 1 then - cursor_pos = 1 - elseif cursor_pos > #self._private.text + 1 then - cursor_pos = #self._private.text + 1 - end - self.p.widget:get_children_by_id('text_image')[1].image = self:draw_text_surface() - self:emit_signal('inputbox::key_pressed', modifiers, key) - end, - } -end - ---[[ - take the text and cursor position and figure out which character is next - if mx is greater than lx then the right character is the next character - if mx is less than lx then the left character is the next character - - calculate the delta between mx and lx, if the delta is greater than the next - character increase or decrease the cursor position by 1 (depends if the delta is positive or negative) - and set the lx = mx - - return 1 if advanced to the right, -1 if advanced to the left, 0 if not advanced; and the new lx -]] -function inputbox:advanced_by_character(mx, lx) - local delta = mx - lx - local character - if delta < 0 then - character = self._private.text:sub(self._private.cursor_pos + 1, self._private.cursor_pos + 1) - else - character = self._private.text:sub(self._private.cursor_pos - 1, self._private.cursor_pos - 1) - end - - --local character = (self._private.text:sub(self._private.cursor_pos + 1, self._private.cursor_pos + 1) and (delta < 0)) or self._private.text:sub(self._private.cursor_pos - 1, self._private.cursor_pos - 1) - if character then - local cr = cairo.Context(cairo.ImageSurface(cairo.Format.ARGB32, 1, 1)) - cr:select_font_face(self.font, cairo.FontSlant.NORMAL, cairo.FontWeight.NORMAL) - cr:set_font_size(self.font_size) - local extents = cr:text_extents(character) - if math.abs(delta) >= extents.x_advance then - self._private.cursor_pos = self._private.cursor_pos + (((delta > 0) and 1) or -1) - lx = mx - return (((delta > 0) and 1) or -1), lx +function inputbox:unfocus(reset) + if akeygrabber.is_running then + self._private.akeygrabber:stop() + if reset then + self:set_cursor_pos(0) + self:set_highlight { start_pos = 0, end_pos = 0 } end end - return 0, lx end ---- Creates a new inputbox widget --- @tparam table args Arguments for the inputbox widget --- @tparam string args.text The text to display in the inputbox --- @tparam[opt=beautiful.fg_normal] string args.fg Text foreground color --- @tparam[opt=beautiful.border_focus] string args.border_focus_color Border color when focused --- @tparam[opt=""] string args.placeholder_text placeholder text to be shown when not focused and --- @tparam[opt=beautiful.inputbox_placeholder_fg] string args.placeholder_fg placeholder text foreground color --- @tparam[opt=beautiful.inputbox_cursor_bg] string args.cursor_bg Cursor background color --- @tparam[opt=beautiful.inputbox_cursor_fg] string args.cursor_fg Cursor foreground color --- @tparam[opt=beautiful.inputbox_highlight_bg] string args.highlight_bg Highlight background color --- @tparam[opt=beautiful.inputbox_highlight_fg] string args.highlight_fg Highlight foreground color --- @treturn awful.widget.inputbox The inputbox widget. --- @constructorfct awful.widget.inputbox function inputbox.new(args) - args = args or {} + local ret = gobject { enable_properties = true } + gtable.crush(ret, inputbox) - -- directly pass a possible default text(this is not meant to be a hint) - local w = imagebox() + ret._private = {} + ret._private.context = PangoCairo.font_map_get_default():create_context() + ret._private.layout = Pango.Layout.new(ret._private.context) + ret._private.layout:set_font_description(Pango.FontDescription.from_string('JetBrainsMono Nerd Font 16')) - --gtable.crush(w, args) - gtable.crush(w, inputbox, true) - w._private = {} - - w._private.text = args.text or '' - w.font_size = 24 - w.font = User_config.font.regular - w._private.cursor_pos = args.cursor_pos - w._private.highlight = args.highlight - - w.p = apopup { - widget = { - { - image = w:draw_text_surface(), - resize = false, - valign = 'bottom', - halign = 'center', - widget = wibox.widget.imagebox, - id = 'text_image', - }, - widget = wibox.container.margin, - margins = 20, - }, - bg = '#212121', - visible = true, - screen = 1, - placement = aplacement.centered, + ret._private.text_hint = args.text_hint or '' + ret._private.cursor_pos = { + index = args.cursor_pos or 0, + trailing = 0, + } + ret._private.highlight = args.highlight or { + start_pos = 0, + end_pos = 0, } - w.p.widget:get_children_by_id('text_image')[1]:buttons(gtable.join { - abutton({}, 1, function() - -- Get the mouse coordinates realative to the widget - local x, y = mouse.coords().x - p.x - 20, mouse.coords().y - p.y - 20 -- 20 is the margin on either side - p.widget:get_children_by_id('text_image')[1].image = w:draw_text_surface(x, false) - w.highlight = { start_pos = w.cursor_pos, end_pos = w.cursor_pos } - p.widget:get_children_by_id('text_image')[1].image = w:draw_text_surface(x, false) - if not mousegrabber.isrunning() then - local last_x, advanced, cursor_pos, mx = x, nil, w.cursor_pos, nil - mousegrabber.run(function(m) - mx = m.x - p.x - 20 - if (math.abs(mx - x) > 5) then - -- Returns 1 if the mouse has advanced to the right, -1 if it has advanced to the left - advanced, last_x = w:advanced_by_character(mx, last_x) - if advanced == 1 then - print(cursor_pos, w.highlight.start_pos, w.highlight.end_pos) - if cursor_pos <= w.highlight.start_pos then - if w.highlight.end_pos < #w._private.text then - w.highlight.end_pos = w.highlight.end_pos + 1 - end - else - w.highlight.start_pos = w.highlight.start_pos + 1 - end - p.widget:get_children_by_id('text_image')[1].image = w:draw_text_surface(x, true) - print(w.highlight.start_pos, w.highlight.end_pos) - elseif advanced == -1 then - if cursor_pos >= w.highlight.end_pos then - if w.highlight.start_pos > 1 then - w.highlight.start_pos = w.highlight.start_pos - 1 - end - else - w.highlight.end_pos = w.highlight.end_pos - 1 - end - p.widget:get_children_by_id('text_image')[1].image = w:draw_text_surface(x, true) - print(w.highlight.start_pos, w.highlight.end_pos) - end - end + ret.widget = imagebox(nil, false) - return m.buttons[1] - end, 'xterm') + if args.mouse_focus then + ret.widget:connect_signal('button::press', function(_, x, y, button) + if button == 1 then + ret:set_cursor_pos_from_mouse(x, y) + ret:start_mousegrabber(x, y) + ret:start_keygrabber() end - w:run() - end), - }) + end) + end - --w.font = args.font or beautiful.font - - --w.keybindings = args.keybindings or {} - --w.hint_text = args.hint_text - - --w.markup = args.text or text_with_cursor('', 1, w) - return w + ret:set_text(args.text or '') + return ret end -function inputbox.mt:__call(...) - return inputbox.new(...) -end - -return setmetatable(inputbox, inputbox.mt) +return setmetatable(inputbox, { + __call = function(_, ...) + return inputbox.new(...) + end, +}) diff --git a/awesome/src/modules/inputbox/new.lua b/awesome/src/modules/inputbox/new.lua deleted file mode 100644 index d95af18..0000000 --- a/awesome/src/modules/inputbox/new.lua +++ /dev/null @@ -1,472 +0,0 @@ -local Pango = require('lgi').Pango -local PangoCairo = require('lgi').PangoCairo -local abutton = require('awful.button') -local akey = require('awful.key') -local akeygrabber = require('awful.keygrabber') -local aplacement = require('awful.placement') -local apopup = require('awful.popup') -local base = require('wibox.widget.base') -local beautiful = require('beautiful') -local cairo = require('lgi').cairo -local gobject = require('gears.object') -local gstring = require('gears.string') -local gsurface = require('gears.surface') -local gtable = require('gears.table') -local gtimer = require('gears.timer') -local imagebox = require('wibox.widget.imagebox') -local setmetatable = setmetatable -local textbox = require('wibox.widget.textbox') -local wibox = require('wibox') -local dpi = beautiful.xresources.apply_dpi - -local capi = { - selection = selection, - mousegrabber = mousegrabber, - mouse = mouse, -} - -local inputbox = {} - -local function get_subtext_layout(layout, starti, endi) - local text = layout:get_text() - - local subtext = text:sub(starti, endi) - - local ctx = layout:get_context() - - local sublayout = Pango.Layout.new(ctx) - sublayout:set_font_description(layout:get_font_description()) - sublayout:set_text(subtext) - - local _, sub_extent = sublayout:get_extents() - return sub_extent -end - -function inputbox.draw_text(self) - local text = self:get_text() - local highlight = self:get_highlight() - local fg_color = { 1, 1, 1, 1 } - local cursor_color = { 1, 1, 1, 1 } - - if text == '' then - fg_color = { 0.2, 0.2, 0.2, 1 } - -- Silently change the text, it will be changed back after it is drawn - self._private.layout:set_text(self._private.text_hint) - end - - local _, pango_extent = self._private.layout:get_extents() - - local surface = cairo.ImageSurface(cairo.Format.ARGB32, (pango_extent.width / Pango.SCALE) + pango_extent.x + 2, (pango_extent.height / Pango.SCALE) + pango_extent.y) - local cr = cairo.Context(surface) - - -- Draw highlight - if highlight.start_pos ~= highlight.end_pos then - cr:set_source_rgb(0, 0, 1) - local sub_extent = get_subtext_layout(self._private.layout, self:get_highlight().start_pos, self:get_highlight().end_pos) - local _, x_offset = self._private.layout:index_to_line_x(self:get_highlight().start_pos, false) - cr:rectangle( - x_offset / Pango.SCALE, - pango_extent.y / Pango.SCALE, - sub_extent.width / Pango.SCALE, - pango_extent.height / Pango.SCALE - ) - cr:fill() - end - - -- Draw text - PangoCairo.update_layout(cr, self._private.layout) - cr:set_source_rgba(table.unpack(fg_color)) - cr:move_to(0, 0) - PangoCairo.show_layout(cr, self._private.layout) - - -- Draw cursor - cr:set_source_rgba(table.unpack(cursor_color)) - local cursor = self:get_cursor_pos() - cr:rectangle( - cursor.x / Pango.SCALE, - cursor.y / Pango.SCALE, - 2, - cursor.height / Pango.SCALE) - cr:fill() - - self.widget:set_image(surface) - return surface -end - -function inputbox:start_keygrabber() - self.akeygrabber = akeygrabber { - autostart = true, - stop_key = { 'Escape', 'Return' }, - start_callback = function() - end, - stop_callback = function() - end, - keybindings = { - akey { - modifiers = {}, - key = 'BackSpace', - on_press = function() - local hl = self:get_highlight() - local text = self:get_text() - local cursor_pos = self:get_cursor_index() - if hl.end_pos ~= hl.start_pos then - self:set_text(text:sub(0, hl.start_pos) .. text:sub(hl.end_pos + 1, #text)) - self:set_cursor_pos(hl.start_pos) - self:set_highlight { start_pos = 0, end_pos = 0 } - else - self:set_text(text:sub(1, cursor_pos - 1) .. text:sub(cursor_pos + 1)) - self:set_cursor_pos(cursor_pos - 1) - end - end, - }, - akey { - modifiers = {}, - key = 'Delete', - on_press = function() - local hl = self:get_highlight() - local text = self:get_text() - local cursor_pos = self:get_cursor_index() - if hl.end_pos ~= hl.start_pos then - self:set_text(text:sub(0, hl.start_pos) .. text:sub(hl.end_pos + 1, #text)) - self:set_cursor_pos(hl.start_pos) - self:set_highlight { start_pos = 0, end_pos = 0 } - else - self:set_text(text:sub(1, cursor_pos) .. text:sub(cursor_pos + 2, #text)) - end - end, - }, - akey { - modifiers = {}, - key = 'Left', - on_press = function() - self:set_cursor_pos(self:get_cursor_index() - 1) - self:set_highlight { start_pos = 0, end_pos = 0 } - end, - }, - akey { - modifiers = {}, - key = 'Right', - on_press = function() - self:set_cursor_pos(self:get_cursor_index() + 1) - self:set_highlight { start_pos = 0, end_pos = 0 } - end, - }, - akey { - modifiers = {}, - key = 'Home', - on_press = function() - self:set_cursor_pos(0) - self:set_highlight { start_pos = 0, end_pos = 0 } - end, - }, - akey { - modifiers = {}, - key = 'End', - on_press = function() - self:set_cursor_pos(#self:get_text()) - self:set_highlight { start_pos = 0, end_pos = 0 } - end, - }, - akey { - modifiers = { 'Shift' }, - key = 'Left', - on_press = function() - local cursor_pos = self:get_cursor_index() - local hl = self:get_highlight() - if cursor_pos == hl.start_pos then - self:set_cursor_pos(cursor_pos - 1) - self:set_highlight { start_pos = self:get_cursor_index(), end_pos = hl.end_pos } - elseif cursor_pos == hl.end_pos then - self:set_cursor_pos(cursor_pos - 1) - self:set_highlight { start_pos = hl.start_pos, end_pos = self:get_cursor_index() } - else - if (hl.start_pos ~= cursor_pos) and (hl.end_pos ~= cursor_pos) then - self:set_highlight { start_pos = cursor_pos, end_pos = cursor_pos } - hl = self:get_highlight() - self:set_cursor_pos(cursor_pos - 1) - self:set_highlight { start_pos = self:get_cursor_index(), end_pos = hl.end_pos } - end - end - end, - }, - akey { - modifiers = { 'Shift' }, - key = 'Right', - on_press = function() - local cursor_pos = self:get_cursor_index() - local hl = self:get_highlight() - if cursor_pos == hl.end_pos then - self:set_cursor_pos(cursor_pos + 1) - self:set_highlight { start_pos = hl.start_pos, end_pos = self:get_cursor_index() } - elseif cursor_pos == hl.start_pos then - self:set_cursor_pos(cursor_pos + 1) - self:set_highlight { start_pos = self:get_cursor_index(), end_pos = hl.end_pos } - else - if (hl.start_pos ~= cursor_pos) and (hl.end_pos ~= cursor_pos) then - self:set_highlight { start_pos = cursor_pos, end_pos = cursor_pos } - hl = self:get_highlight() - self:set_cursor_pos(cursor_pos + 1) - self:set_highlight { start_pos = hl.start_pos, end_pos = self:get_cursor_index() } - end - end - end, - }, - akey { - modifiers = { 'Control' }, - key = 'a', - on_press = function() - self:set_highlight { start_pos = 0, end_pos = #self:get_text() } - self:set_cursor_pos(#self:get_text() - 1) - end, - }, - akey { - modifiers = { 'Control' }, - key = 'c', - on_press = function() - local hl = self:get_highlight() - if hl.start_pos ~= hl.end_pos then - local text = self:get_text():sub(hl.start_pos, hl.end_pos) - --TODO:self:copy_to_clipboard(text) - end - end, - }, - akey { - modifiers = { 'Control' }, - key = 'v', - on_press = function() - local hl = self:get_highlight() - local selection = capi.selection() - if hl.start_pos ~= hl.end_pos then - self:set_text(self:get_text():sub(1, hl.start_pos) .. selection .. self:get_text():sub(hl.end_pos + 1, #self:get_text())) - self:set_cursor_pos(hl.start_pos + #selection) - self:set_highlight { start_pos = 0, end_pos = 0 } - else - self:set_text(self:get_text():sub(1, self:get_cursor_pos()) .. selection .. self:get_text():sub(self:get_cursor_pos() + 1, #self:get_text())) - self:set_cursor_pos(self:get_cursor_pos() + #selection) - end - end, - }, - akey { - modifiers = { 'Control' }, - key = 'x', - on_press = function() - --TODO - end, - }, - akey { - modifiers = { 'Control' }, - key = 'Right', - on_press = function() - - end, - }, - akey { - modifiers = { 'Control' }, - key = 'Left', - on_press = function() - - end, - }, - }, - keypressed_callback = function(_, mod, key) - local text = self:get_text() - local cursor_pos = self:get_cursor_index() - if (mod[1] == 'Mod2' or '') and (key:wlen() == 1) then - self:set_text(text:sub(1, cursor_pos) .. key .. text:sub(cursor_pos + 1, #text)) - self:set_cursor_pos(cursor_pos + #key) - elseif (mod[1] == 'Shift') and (key:wlen() == 1) then - self:set_text(text:sub(1, cursor_pos) .. key:upper() .. text:sub(cursor_pos + 1, #text)) - self:set_cursor_pos(cursor_pos + #key) - end - end, - } -end - -function inputbox:start_mousegrabber(x, y) - --[[ if not mousegrabber.isrunning() then - local last_x, advanced, cursor_pos, mx = x, nil, self:get_cursor_pos(), nil - local hl = self:get_highlight() - self:set_highlight { start_pos = cursor_pos, end_pos = cursor_pos } - local text = self:get_text() - mousegrabber.run(function(m) - mx = m.x - if (math.abs(mx - x) > 5) then - advanced, last_x = self:advanced_by_character(mx, last_x) - if advanced == 1 then - if cursor_pos <= hl.start_pos then - if hl.end_pos < #text then - self:set_highlight { start_pos = hl.start_pos, end_pos = hl.end_pos + 1 } - end - else - self:set_highlight { start_pos = hl.start_pos + 1, end_pos = hl.end_pos } - end - elseif advanced == -1 then - if cursor_pos >= hl.end_pos then - if hl.start_pos > 1 then - self:set_highlight { start_pos = hl.start_pos - 1, end_pos = hl.end_pos } - end - else - self:set_highlight { start_pos = hl.start_pos, end_pos = hl.end_pos - 1 } - end - end - end - hl = self:get_highlight() - return m.buttons[1] - end, 'xterm') - end ]] - if not mousegrabber.isrunning() then - local index, _ = self._private.layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE) - - if not index then index = #self:get_text() end - - self:set_cursor_pos(index) - -- Remove highlight, but also prepare its position (same pos = no highlight) - self:set_highlight { start_pos = index, end_pos = index } - - local text = self:get_text() - local hl = self:get_highlight() - - local mb_start = index - local m_start_x = capi.mouse.coords().x - - mousegrabber.run(function(m) - index, _ = self._private.layout:xy_to_index((m.x - m_start_x + x) * Pango.SCALE, y * Pango.SCALE) - - if not index then index = #text end - - if mb_start - index == 1 then - if index <= hl.start_pos then - if hl.end_pos < #text then - self:set_highlight { start_pos = hl.start_pos, end_pos = hl.end_pos + 1 } - end - else - self:set_highlight { start_pos = hl.start_pos + 1, end_pos = hl.end_pos } - end - elseif mb_start - index == -1 then - if index >= hl.end_pos then - if hl.start_pos > 1 then - self:set_highlight { start_pos = hl.start_pos - 1, end_pos = hl.end_pos } - end - else - self:set_highlight { start_pos = hl.start_pos, end_pos = hl.end_pos - 1 } - end - end - - if index ~= mb_start then - self:set_cursor_pos(index) - mb_start = index - end - print(self:get_highlight().start_pos, self:get_highlight().end_pos) - hl = self:get_highlight() - return m.buttons[1] - end, 'xterm') - end -end - -function inputbox:set_cursor_pos_from_mouse(x, y) - -- When setting the cursor position, trailing is not needed as its handled by the setter - local index = self._private.layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE) - if not index then index = #self:get_text() end - - self:set_highlight { start_pos = 0, end_pos = 0 } - self:set_cursor_pos(index) -end - -function inputbox:get_text() - return self._private.layout:get_text() -end - -function inputbox:set_text(text) - if self:get_text() == text then return end - - local attributes, parsed = Pango.parse_markup(text, -1, 0) - - if not attributes then return parsed.message or tostring(parsed) end - - self._private.layout:set_text(parsed, string.len(parsed)) - self._private.layout:set_attributes(attributes) - - self.draw_text(self) -end - -function inputbox:get_cursor_pos() - return self._private.layout:get_cursor_pos(self._private.cursor_pos.index) -end - -function inputbox:get_cursor_index() - return self._private.cursor_pos.index -end - -function inputbox:set_cursor_pos(cursor_pos) - -- moving only moved one character set, to move it multiple times we need to loop as long as the difference to the new cursor isn't 0 - if not cursor_pos or (cursor_pos < 0) or (cursor_pos > #self:get_text()) then return end - --while (cursor_pos - self._private.cursor_pos.index) ~= 0 do - --[[ self._private.cursor_pos.index, self._private.cursor_pos.trailing = self._private.layout:move_cursor_visually( - true, self._private.cursor_pos.index, - self._private.cursor_pos.trailing, - cursor_pos - self._private.cursor_pos.index - ) ]] - self._private.cursor_pos.index = cursor_pos - --end - - self.draw_text(self) -end - -function inputbox:get_highlight() - return self._private.highlight -end - -function inputbox:set_highlight(highlight) - self._private.highlight = highlight - self.draw_text(self) -end - -function inputbox:focus() - -end - -function inputbox:unfocus() - -end - -function inputbox.new(args) - local ret = gobject { enable_properties = true } - gtable.crush(ret, inputbox) - - ret._private = {} - ret._private.context = PangoCairo.font_map_get_default():create_context() - ret._private.layout = Pango.Layout.new(ret._private.context) - ret._private.layout:set_font_description(Pango.FontDescription.from_string('JetBrainsMono Nerd Font 16')) - - ret._private.text_hint = args.text_hint or '' - ret._private.cursor_pos = { - index = args.cursor_pos or 0, - trailing = 0, - } - ret._private.highlight = args.highlight or { - start_pos = 0, - end_pos = 0, - } - - ret.widget = imagebox(nil, false) - - ret.widget:connect_signal('button::press', function(_, x, y, button) - if button == 1 then - ret:set_cursor_pos_from_mouse(x, y) - ret:start_mousegrabber(x, y) - ret:start_keygrabber() - end - end) - - ret:set_text(args.text or '') - --ret:set_cursor_pos(ret._private.cursor_pos) - ret:set_highlight(ret._private.highlight) - - return ret -end - -return setmetatable(inputbox, { - __call = function(_, ...) - return inputbox.new(...) - end, -})