entry.go 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786
  1. package widget
  2. import (
  3. "image/color"
  4. "math"
  5. "strings"
  6. "unicode"
  7. "fyne.io/fyne/v2"
  8. "fyne.io/fyne/v2/canvas"
  9. "fyne.io/fyne/v2/data/binding"
  10. "fyne.io/fyne/v2/driver/desktop"
  11. "fyne.io/fyne/v2/driver/mobile"
  12. "fyne.io/fyne/v2/internal/cache"
  13. "fyne.io/fyne/v2/internal/widget"
  14. "fyne.io/fyne/v2/theme"
  15. )
  16. const (
  17. multiLineRows = 3
  18. doubleClickWordSeperator = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?"
  19. )
  20. // Declare conformity with interfaces
  21. var _ fyne.Disableable = (*Entry)(nil)
  22. var _ fyne.Draggable = (*Entry)(nil)
  23. var _ fyne.Focusable = (*Entry)(nil)
  24. var _ fyne.Tappable = (*Entry)(nil)
  25. var _ fyne.Widget = (*Entry)(nil)
  26. var _ desktop.Mouseable = (*Entry)(nil)
  27. var _ desktop.Keyable = (*Entry)(nil)
  28. var _ mobile.Keyboardable = (*Entry)(nil)
  29. var _ mobile.Touchable = (*Entry)(nil)
  30. var _ fyne.Tabbable = (*Entry)(nil)
  31. // Entry widget allows simple text to be input when focused.
  32. type Entry struct {
  33. DisableableWidget
  34. shortcut fyne.ShortcutHandler
  35. Text string
  36. // Since: 2.0
  37. TextStyle fyne.TextStyle
  38. PlaceHolder string
  39. OnChanged func(string) `json:"-"`
  40. // Since: 2.0
  41. OnSubmitted func(string) `json:"-"`
  42. Password bool
  43. MultiLine bool
  44. Wrapping fyne.TextWrap
  45. // Set a validator that this entry will check against
  46. // Since: 1.4
  47. Validator fyne.StringValidator `json:"-"`
  48. validationStatus *validationStatus
  49. onValidationChanged func(error)
  50. validationError error
  51. CursorRow, CursorColumn int
  52. OnCursorChanged func() `json:"-"`
  53. cursorAnim *entryCursorAnimation
  54. dirty bool
  55. focused bool
  56. text *RichText
  57. placeholder *RichText
  58. content *entryContent
  59. scroll *widget.Scroll
  60. // useful for Form validation (as the error text should only be shown when
  61. // the entry is unfocused)
  62. onFocusChanged func(bool)
  63. // selectRow and selectColumn represent the selection start location
  64. // The selection will span from selectRow/Column to CursorRow/Column -- note that the cursor
  65. // position may occur before or after the select start position in the text.
  66. selectRow, selectColumn int
  67. // selectKeyDown indicates whether left shift or right shift is currently held down
  68. selectKeyDown bool
  69. // selecting indicates whether the cursor has moved since it was at the selection start location
  70. selecting bool
  71. popUp *PopUpMenu
  72. // TODO: Add OnSelectChanged
  73. // ActionItem is a small item which is displayed at the outer right of the entry (like a password revealer)
  74. ActionItem fyne.CanvasObject `json:"-"`
  75. binder basicBinder
  76. conversionError error
  77. multiLineRows int // override global default number of visible lines
  78. }
  79. // NewEntry creates a new single line entry widget.
  80. func NewEntry() *Entry {
  81. e := &Entry{Wrapping: fyne.TextTruncate}
  82. e.ExtendBaseWidget(e)
  83. return e
  84. }
  85. // NewEntryWithData returns an Entry widget connected to the specified data source.
  86. //
  87. // Since: 2.0
  88. func NewEntryWithData(data binding.String) *Entry {
  89. entry := NewEntry()
  90. entry.Bind(data)
  91. return entry
  92. }
  93. // NewMultiLineEntry creates a new entry that allows multiple lines
  94. func NewMultiLineEntry() *Entry {
  95. e := &Entry{MultiLine: true, Wrapping: fyne.TextTruncate}
  96. e.ExtendBaseWidget(e)
  97. return e
  98. }
  99. // NewPasswordEntry creates a new entry password widget
  100. func NewPasswordEntry() *Entry {
  101. e := &Entry{Password: true, Wrapping: fyne.TextTruncate}
  102. e.ExtendBaseWidget(e)
  103. e.ActionItem = newPasswordRevealer(e)
  104. return e
  105. }
  106. // AcceptsTab returns if Entry accepts the Tab key or not.
  107. //
  108. // Implements: fyne.Tabbable
  109. //
  110. // Since: 2.1
  111. func (e *Entry) AcceptsTab() bool {
  112. return e.MultiLine
  113. }
  114. // Bind connects the specified data source to this Entry.
  115. // The current value will be displayed and any changes in the data will cause the widget to update.
  116. // User interactions with this Entry will set the value into the data source.
  117. //
  118. // Since: 2.0
  119. func (e *Entry) Bind(data binding.String) {
  120. e.binder.SetCallback(e.updateFromData)
  121. e.binder.Bind(data)
  122. e.Validator = func(string) error {
  123. return e.conversionError
  124. }
  125. e.OnChanged = func(_ string) {
  126. e.binder.CallWithData(e.writeData)
  127. e.Validate()
  128. }
  129. }
  130. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  131. //
  132. // Implements: fyne.Widget
  133. func (e *Entry) CreateRenderer() fyne.WidgetRenderer {
  134. e.ExtendBaseWidget(e)
  135. // initialise
  136. e.textProvider()
  137. e.placeholderProvider()
  138. box := canvas.NewRectangle(theme.InputBackgroundColor())
  139. border := canvas.NewRectangle(color.Transparent)
  140. border.StrokeWidth = theme.InputBorderSize()
  141. border.StrokeColor = theme.InputBorderColor()
  142. cursor := canvas.NewRectangle(color.Transparent)
  143. cursor.Hide()
  144. e.cursorAnim = newEntryCursorAnimation(cursor)
  145. e.content = &entryContent{entry: e}
  146. e.scroll = widget.NewScroll(nil)
  147. objects := []fyne.CanvasObject{box, border}
  148. if e.Wrapping != fyne.TextWrapOff {
  149. e.scroll.Content = e.content
  150. objects = append(objects, e.scroll)
  151. } else {
  152. e.scroll.Hide()
  153. objects = append(objects, e.content)
  154. }
  155. e.content.scroll = e.scroll
  156. if e.Password && e.ActionItem == nil {
  157. // An entry widget has been created via struct setting manually
  158. // the Password field to true. Going to enable the password revealer.
  159. e.ActionItem = newPasswordRevealer(e)
  160. }
  161. if e.ActionItem != nil {
  162. objects = append(objects, e.ActionItem)
  163. }
  164. return &entryRenderer{box, border, e.scroll, objects, e}
  165. }
  166. // Cursor returns the cursor type of this widget
  167. //
  168. // Implements: desktop.Cursorable
  169. func (e *Entry) Cursor() desktop.Cursor {
  170. return desktop.TextCursor
  171. }
  172. // Disable this widget so that it cannot be interacted with, updating any style appropriately.
  173. //
  174. // Implements: fyne.Disableable
  175. func (e *Entry) Disable() {
  176. e.DisableableWidget.Disable()
  177. }
  178. // Disabled returns whether the entry is disabled or read-only.
  179. //
  180. // Implements: fyne.Disableable
  181. func (e *Entry) Disabled() bool {
  182. return e.DisableableWidget.disabled
  183. }
  184. // DoubleTapped is called when this entry has been double tapped so we should select text below the pointer
  185. //
  186. // Implements: fyne.DoubleTappable
  187. func (e *Entry) DoubleTapped(p *fyne.PointEvent) {
  188. row := e.textProvider().row(e.CursorRow)
  189. start, end := getTextWhitespaceRegion(row, e.CursorColumn)
  190. if start == -1 || end == -1 {
  191. return
  192. }
  193. e.setFieldsAndRefresh(func() {
  194. if !e.selectKeyDown {
  195. e.selectRow = e.CursorRow
  196. e.selectColumn = start
  197. }
  198. // Always aim to maximise the selected region
  199. if e.selectRow > e.CursorRow || (e.selectRow == e.CursorRow && e.selectColumn > e.CursorColumn) {
  200. e.CursorColumn = start
  201. } else {
  202. e.CursorColumn = end
  203. }
  204. e.selecting = true
  205. })
  206. }
  207. // DragEnd is called at end of a drag event.
  208. //
  209. // Implements: fyne.Draggable
  210. func (e *Entry) DragEnd() {
  211. e.propertyLock.Lock()
  212. if e.CursorColumn == e.selectColumn && e.CursorRow == e.selectRow {
  213. e.selecting = false
  214. }
  215. shouldRefresh := !e.selecting
  216. e.propertyLock.Unlock()
  217. if shouldRefresh {
  218. e.Refresh()
  219. }
  220. }
  221. // Dragged is called when the pointer moves while a button is held down.
  222. // It updates the selection accordingly.
  223. //
  224. // Implements: fyne.Draggable
  225. func (e *Entry) Dragged(d *fyne.DragEvent) {
  226. pos := d.Position.Subtract(e.scroll.Offset).Add(fyne.NewPos(0, theme.InputBorderSize()))
  227. if !e.selecting {
  228. startPos := pos.Subtract(d.Dragged)
  229. e.selectRow, e.selectColumn = e.getRowCol(startPos)
  230. e.selecting = true
  231. }
  232. e.updateMousePointer(pos, false)
  233. }
  234. // Enable this widget, updating any style or features appropriately.
  235. //
  236. // Implements: fyne.Disableable
  237. func (e *Entry) Enable() {
  238. e.DisableableWidget.Enable()
  239. }
  240. // ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
  241. func (e *Entry) ExtendBaseWidget(wid fyne.Widget) {
  242. impl := e.super()
  243. if impl != nil {
  244. return
  245. }
  246. e.propertyLock.Lock()
  247. defer e.propertyLock.Unlock()
  248. e.BaseWidget.impl = wid
  249. e.registerShortcut()
  250. }
  251. // FocusGained is called when the Entry has been given focus.
  252. //
  253. // Implements: fyne.Focusable
  254. func (e *Entry) FocusGained() {
  255. e.setFieldsAndRefresh(func() {
  256. e.dirty = true
  257. e.focused = true
  258. })
  259. if e.onFocusChanged != nil {
  260. e.onFocusChanged(true)
  261. }
  262. }
  263. // FocusLost is called when the Entry has had focus removed.
  264. //
  265. // Implements: fyne.Focusable
  266. func (e *Entry) FocusLost() {
  267. e.setFieldsAndRefresh(func() {
  268. e.focused = false
  269. e.selectKeyDown = false
  270. })
  271. if e.onFocusChanged != nil {
  272. e.onFocusChanged(false)
  273. }
  274. }
  275. // Hide hides the entry.
  276. //
  277. // Implements: fyne.Widget
  278. func (e *Entry) Hide() {
  279. if e.popUp != nil {
  280. e.popUp.Hide()
  281. e.popUp = nil
  282. }
  283. e.DisableableWidget.Hide()
  284. }
  285. // Keyboard implements the Keyboardable interface
  286. //
  287. // Implements: mobile.Keyboardable
  288. func (e *Entry) Keyboard() mobile.KeyboardType {
  289. e.propertyLock.RLock()
  290. defer e.propertyLock.RUnlock()
  291. if e.MultiLine {
  292. return mobile.DefaultKeyboard
  293. } else if e.Password {
  294. return mobile.PasswordKeyboard
  295. }
  296. return mobile.SingleLineKeyboard
  297. }
  298. // KeyDown handler for keypress events - used to store shift modifier state for text selection
  299. //
  300. // Implements: desktop.Keyable
  301. func (e *Entry) KeyDown(key *fyne.KeyEvent) {
  302. if e.Disabled() {
  303. return
  304. }
  305. // For keyboard cursor controlled selection we now need to store shift key state and selection "start"
  306. // Note: selection start is where the highlight started (if the user moves the selection up or left then
  307. // the selectRow/Column will not match SelectionStart)
  308. if key.Name == desktop.KeyShiftLeft || key.Name == desktop.KeyShiftRight {
  309. if !e.selecting {
  310. e.selectRow = e.CursorRow
  311. e.selectColumn = e.CursorColumn
  312. }
  313. e.selectKeyDown = true
  314. }
  315. }
  316. // KeyUp handler for key release events - used to reset shift modifier state for text selection
  317. //
  318. // Implements: desktop.Keyable
  319. func (e *Entry) KeyUp(key *fyne.KeyEvent) {
  320. if e.Disabled() {
  321. return
  322. }
  323. // Handle shift release for keyboard selection
  324. // Note: if shift is released then the user may repress it without moving to adjust their old selection
  325. if key.Name == desktop.KeyShiftLeft || key.Name == desktop.KeyShiftRight {
  326. e.selectKeyDown = false
  327. }
  328. }
  329. // MinSize returns the size that this widget should not shrink below.
  330. //
  331. // Implements: fyne.Widget
  332. func (e *Entry) MinSize() fyne.Size {
  333. e.ExtendBaseWidget(e)
  334. min := e.BaseWidget.MinSize()
  335. if e.ActionItem != nil {
  336. min = min.Add(fyne.NewSize(theme.IconInlineSize()+theme.LineSpacing(), 0))
  337. }
  338. if e.Validator != nil {
  339. min = min.Add(fyne.NewSize(theme.IconInlineSize()+theme.LineSpacing(), 0))
  340. }
  341. return min
  342. }
  343. // MouseDown called on mouse click, this triggers a mouse click which can move the cursor,
  344. // update the existing selection (if shift is held), or start a selection dragging operation.
  345. //
  346. // Implements: desktop.Mouseable
  347. func (e *Entry) MouseDown(m *desktop.MouseEvent) {
  348. e.propertyLock.Lock()
  349. if e.selectKeyDown {
  350. e.selecting = true
  351. }
  352. if e.selecting && !e.selectKeyDown && m.Button == desktop.MouseButtonPrimary {
  353. e.selecting = false
  354. }
  355. e.propertyLock.Unlock()
  356. e.updateMousePointer(m.Position, m.Button == desktop.MouseButtonSecondary)
  357. if !e.Disabled() {
  358. e.requestFocus()
  359. }
  360. }
  361. // MouseUp called on mouse release
  362. // If a mouse drag event has completed then check to see if it has resulted in an empty selection,
  363. // if so, and if a text select key isn't held, then disable selecting
  364. //
  365. // Implements: desktop.Mouseable
  366. func (e *Entry) MouseUp(m *desktop.MouseEvent) {
  367. start, _ := e.selection()
  368. e.propertyLock.Lock()
  369. defer e.propertyLock.Unlock()
  370. if start == -1 && e.selecting && !e.selectKeyDown {
  371. e.selecting = false
  372. }
  373. }
  374. // SelectedText returns the text currently selected in this Entry.
  375. // If there is no selection it will return the empty string.
  376. func (e *Entry) SelectedText() string {
  377. e.propertyLock.RLock()
  378. selecting := e.selecting
  379. e.propertyLock.RUnlock()
  380. if !selecting {
  381. return ""
  382. }
  383. start, stop := e.selection()
  384. if start == stop {
  385. return ""
  386. }
  387. e.propertyLock.RLock()
  388. defer e.propertyLock.RUnlock()
  389. r := ([]rune)(e.textProvider().String())
  390. return string(r[start:stop])
  391. }
  392. // SetMinRowsVisible forces a multi-line entry to show `count` number of rows without scrolling.
  393. // This is not a validation or requirement, it just impacts the minimum visible size.
  394. // Use this carefully as Fyne apps can run on small screens so you may wish to add a scroll container if
  395. // this number is high. Default is 3.
  396. //
  397. // Since: 2.2
  398. func (e *Entry) SetMinRowsVisible(count int) {
  399. e.multiLineRows = count
  400. }
  401. // SetPlaceHolder sets the text that will be displayed if the entry is otherwise empty
  402. func (e *Entry) SetPlaceHolder(text string) {
  403. e.propertyLock.Lock()
  404. e.PlaceHolder = text
  405. e.propertyLock.Unlock()
  406. e.placeholderProvider().Segments[0].(*TextSegment).Text = text
  407. e.placeholder.updateRowBounds()
  408. e.placeholderProvider().Refresh()
  409. }
  410. // SetText manually sets the text of the Entry to the given text value.
  411. func (e *Entry) SetText(text string) {
  412. e.updateText(text)
  413. e.updateCursorAndSelection()
  414. }
  415. // Tapped is called when this entry has been tapped. We update the cursor position in
  416. // device-specific callbacks (MouseDown() and TouchDown()).
  417. //
  418. // Implements: fyne.Tappable
  419. func (e *Entry) Tapped(ev *fyne.PointEvent) {
  420. if fyne.CurrentDevice().IsMobile() && e.selecting {
  421. e.selecting = false
  422. }
  423. }
  424. // TappedSecondary is called when right or alternative tap is invoked.
  425. //
  426. // Opens the PopUpMenu with `Paste` item to paste text from the clipboard.
  427. //
  428. // Implements: fyne.SecondaryTappable
  429. func (e *Entry) TappedSecondary(pe *fyne.PointEvent) {
  430. if e.Disabled() && e.Password {
  431. return // no popup options for a disabled concealed field
  432. }
  433. e.requestFocus()
  434. clipboard := fyne.CurrentApp().Driver().AllWindows()[0].Clipboard()
  435. super := e.super()
  436. cutItem := fyne.NewMenuItem("Cut", func() {
  437. super.(fyne.Shortcutable).TypedShortcut(&fyne.ShortcutCut{Clipboard: clipboard})
  438. })
  439. copyItem := fyne.NewMenuItem("Copy", func() {
  440. super.(fyne.Shortcutable).TypedShortcut(&fyne.ShortcutCopy{Clipboard: clipboard})
  441. })
  442. pasteItem := fyne.NewMenuItem("Paste", func() {
  443. super.(fyne.Shortcutable).TypedShortcut(&fyne.ShortcutPaste{Clipboard: clipboard})
  444. })
  445. selectAllItem := fyne.NewMenuItem("Select all", e.selectAll)
  446. entryPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(super)
  447. popUpPos := entryPos.Add(fyne.NewPos(pe.Position.X, pe.Position.Y))
  448. c := fyne.CurrentApp().Driver().CanvasForObject(super)
  449. var menu *fyne.Menu
  450. if e.Disabled() {
  451. menu = fyne.NewMenu("", copyItem, selectAllItem)
  452. } else if e.Password {
  453. menu = fyne.NewMenu("", pasteItem, selectAllItem)
  454. } else {
  455. menu = fyne.NewMenu("", cutItem, copyItem, pasteItem, selectAllItem)
  456. }
  457. e.popUp = NewPopUpMenu(menu, c)
  458. e.popUp.ShowAtPosition(popUpPos)
  459. }
  460. // TouchDown is called when this entry gets a touch down event on mobile device, we ensure we have focus.
  461. //
  462. // Since: 2.1
  463. //
  464. // Implements: mobile.Touchable
  465. func (e *Entry) TouchDown(ev *mobile.TouchEvent) {
  466. if !e.Disabled() {
  467. e.requestFocus()
  468. }
  469. e.updateMousePointer(ev.Position, false)
  470. }
  471. // TouchUp is called when this entry gets a touch up event on mobile device.
  472. //
  473. // Since: 2.1
  474. //
  475. // Implements: mobile.Touchable
  476. func (e *Entry) TouchUp(*mobile.TouchEvent) {
  477. }
  478. // TouchCancel is called when this entry gets a touch cancel event on mobile device (app was removed from focus).
  479. //
  480. // Since: 2.1
  481. //
  482. // Implements: mobile.Touchable
  483. func (e *Entry) TouchCancel(*mobile.TouchEvent) {
  484. }
  485. // TypedKey receives key input events when the Entry widget is focused.
  486. //
  487. // Implements: fyne.Focusable
  488. func (e *Entry) TypedKey(key *fyne.KeyEvent) {
  489. if e.Disabled() {
  490. return
  491. }
  492. if e.cursorAnim != nil {
  493. e.cursorAnim.interrupt()
  494. }
  495. e.propertyLock.RLock()
  496. provider := e.textProvider()
  497. multiLine := e.MultiLine
  498. e.propertyLock.RUnlock()
  499. if e.selectKeyDown || e.selecting {
  500. if e.selectingKeyHandler(key) {
  501. e.Refresh()
  502. return
  503. }
  504. }
  505. switch key.Name {
  506. case fyne.KeyBackspace:
  507. e.propertyLock.RLock()
  508. isEmpty := provider.len() == 0 || (e.CursorColumn == 0 && e.CursorRow == 0)
  509. e.propertyLock.RUnlock()
  510. if isEmpty {
  511. return
  512. }
  513. e.propertyLock.Lock()
  514. pos := e.cursorTextPos()
  515. provider.deleteFromTo(pos-1, pos)
  516. e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos - 1)
  517. e.propertyLock.Unlock()
  518. case fyne.KeyDelete:
  519. pos := e.cursorTextPos()
  520. if provider.len() == 0 || pos == provider.len() {
  521. return
  522. }
  523. e.propertyLock.Lock()
  524. provider.deleteFromTo(pos, pos+1)
  525. e.propertyLock.Unlock()
  526. case fyne.KeyReturn, fyne.KeyEnter:
  527. e.typedKeyReturn(provider, multiLine)
  528. case fyne.KeyTab:
  529. e.TypedRune('\t')
  530. case fyne.KeyUp:
  531. e.typedKeyUp(provider, multiLine)
  532. case fyne.KeyDown:
  533. e.typedKeyDown(provider, multiLine)
  534. case fyne.KeyLeft:
  535. e.typedKeyLeft(provider, multiLine)
  536. case fyne.KeyRight:
  537. e.typedKeyRight(provider, multiLine)
  538. case fyne.KeyEnd:
  539. e.propertyLock.Lock()
  540. if e.MultiLine {
  541. e.CursorColumn = provider.rowLength(e.CursorRow)
  542. } else {
  543. e.CursorColumn = provider.len()
  544. }
  545. e.propertyLock.Unlock()
  546. case fyne.KeyHome:
  547. e.propertyLock.Lock()
  548. e.CursorColumn = 0
  549. e.propertyLock.Unlock()
  550. case fyne.KeyPageUp:
  551. e.propertyLock.Lock()
  552. if e.MultiLine {
  553. e.CursorRow = 0
  554. }
  555. e.CursorColumn = 0
  556. e.propertyLock.Unlock()
  557. case fyne.KeyPageDown:
  558. e.propertyLock.Lock()
  559. if e.MultiLine {
  560. e.CursorRow = provider.rows() - 1
  561. e.CursorColumn = provider.rowLength(e.CursorRow)
  562. } else {
  563. e.CursorColumn = provider.len()
  564. }
  565. e.propertyLock.Unlock()
  566. default:
  567. return
  568. }
  569. e.propertyLock.Lock()
  570. if e.CursorRow == e.selectRow && e.CursorColumn == e.selectColumn {
  571. e.selecting = false
  572. }
  573. e.propertyLock.Unlock()
  574. e.updateText(provider.String())
  575. }
  576. func (e *Entry) typedKeyUp(provider *RichText, multiLine bool) {
  577. e.propertyLock.Lock()
  578. if e.CursorRow > 0 {
  579. e.CursorRow--
  580. } else {
  581. e.CursorColumn = 0
  582. }
  583. rowLength := provider.rowLength(e.CursorRow)
  584. if e.CursorColumn > rowLength {
  585. e.CursorColumn = rowLength
  586. }
  587. e.propertyLock.Unlock()
  588. }
  589. func (e *Entry) typedKeyDown(provider *RichText, multiLine bool) {
  590. e.propertyLock.Lock()
  591. rowLength := provider.rowLength(e.CursorRow)
  592. if e.CursorRow < provider.rows()-1 {
  593. e.CursorRow++
  594. rowLength = provider.rowLength(e.CursorRow)
  595. } else {
  596. e.CursorColumn = rowLength
  597. }
  598. if e.CursorColumn > rowLength {
  599. e.CursorColumn = rowLength
  600. }
  601. e.propertyLock.Unlock()
  602. }
  603. func (e *Entry) typedKeyLeft(provider *RichText, multiLine bool) {
  604. e.propertyLock.Lock()
  605. if e.CursorColumn > 0 {
  606. e.CursorColumn--
  607. } else if e.MultiLine && e.CursorRow > 0 {
  608. e.CursorRow--
  609. e.CursorColumn = provider.rowLength(e.CursorRow)
  610. }
  611. e.propertyLock.Unlock()
  612. }
  613. func (e *Entry) typedKeyRight(provider *RichText, multiLine bool) {
  614. e.propertyLock.Lock()
  615. if e.MultiLine {
  616. rowLength := provider.rowLength(e.CursorRow)
  617. if e.CursorColumn < rowLength {
  618. e.CursorColumn++
  619. } else if e.CursorRow < provider.rows()-1 {
  620. e.CursorRow++
  621. e.CursorColumn = 0
  622. }
  623. } else if e.CursorColumn < provider.len() {
  624. e.CursorColumn++
  625. }
  626. e.propertyLock.Unlock()
  627. }
  628. // TypedRune receives text input events when the Entry widget is focused.
  629. //
  630. // Implements: fyne.Focusable
  631. func (e *Entry) TypedRune(r rune) {
  632. if e.Disabled() {
  633. return
  634. }
  635. e.propertyLock.Lock()
  636. if e.popUp != nil {
  637. e.popUp.Hide()
  638. }
  639. selecting := e.selecting
  640. e.propertyLock.Unlock()
  641. // if we've typed a character and we're selecting then replace the selection with the character
  642. if selecting {
  643. cb := e.OnChanged
  644. e.OnChanged = nil // don't propagate this change to binding etc
  645. e.eraseSelection()
  646. e.OnChanged = cb // the change later will then trigger callback
  647. }
  648. e.propertyLock.Lock()
  649. provider := e.textProvider()
  650. e.selecting = false
  651. runes := []rune{r}
  652. pos := e.cursorTextPos()
  653. provider.insertAt(pos, string(runes))
  654. e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos + len(runes))
  655. content := provider.String()
  656. e.propertyLock.Unlock()
  657. e.updateText(content)
  658. }
  659. // TypedShortcut implements the Shortcutable interface
  660. //
  661. // Implements: fyne.Shortcutable
  662. func (e *Entry) TypedShortcut(shortcut fyne.Shortcut) {
  663. e.shortcut.TypedShortcut(shortcut)
  664. }
  665. // Unbind disconnects any configured data source from this Entry.
  666. // The current value will remain at the last value of the data source.
  667. //
  668. // Since: 2.0
  669. func (e *Entry) Unbind() {
  670. e.OnChanged = nil
  671. e.Validator = nil
  672. e.binder.Unbind()
  673. }
  674. // copyToClipboard copies the current selection to a given clipboard.
  675. // This does nothing if it is a concealed entry.
  676. func (e *Entry) copyToClipboard(clipboard fyne.Clipboard) {
  677. if !e.selecting || e.Password {
  678. return
  679. }
  680. clipboard.SetContent(e.SelectedText())
  681. }
  682. func (e *Entry) cursorColAt(text []rune, pos fyne.Position) int {
  683. for i := 0; i < len(text); i++ {
  684. str := string(text[0:i])
  685. wid := fyne.MeasureText(str, theme.TextSize(), e.TextStyle).Width
  686. charWid := fyne.MeasureText(string(text[i]), theme.TextSize(), e.TextStyle).Width
  687. if pos.X < theme.InnerPadding()+wid+(charWid/2) {
  688. return i
  689. }
  690. }
  691. return len(text)
  692. }
  693. func (e *Entry) cursorTextPos() (pos int) {
  694. return e.textPosFromRowCol(e.CursorRow, e.CursorColumn)
  695. }
  696. // copyToClipboard copies the current selection to a given clipboard and then removes the selected text.
  697. // This does nothing if it is a concealed entry.
  698. func (e *Entry) cutToClipboard(clipboard fyne.Clipboard) {
  699. if !e.selecting || e.Password {
  700. return
  701. }
  702. e.copyToClipboard(clipboard)
  703. e.eraseSelection()
  704. }
  705. // eraseSelection removes the current selected region and moves the cursor
  706. func (e *Entry) eraseSelection() {
  707. if e.Disabled() {
  708. return
  709. }
  710. provider := e.textProvider()
  711. posA, posB := e.selection()
  712. if posA == posB {
  713. return
  714. }
  715. e.propertyLock.Lock()
  716. provider.deleteFromTo(posA, posB)
  717. e.CursorRow, e.CursorColumn = e.rowColFromTextPos(posA)
  718. e.selectRow, e.selectColumn = e.CursorRow, e.CursorColumn
  719. e.selecting = false
  720. e.propertyLock.Unlock()
  721. e.updateText(provider.String())
  722. }
  723. func (e *Entry) getRowCol(p fyne.Position) (int, int) {
  724. e.propertyLock.RLock()
  725. defer e.propertyLock.RUnlock()
  726. rowHeight := e.textProvider().charMinSize(e.Password, e.TextStyle).Height
  727. row := int(math.Floor(float64(p.Y+e.scroll.Offset.Y-theme.LineSpacing()) / float64(rowHeight)))
  728. col := 0
  729. if row < 0 {
  730. row = 0
  731. } else if row >= e.textProvider().rows() {
  732. row = e.textProvider().rows() - 1
  733. col = e.textProvider().rowLength(row)
  734. } else {
  735. col = e.cursorColAt(e.textProvider().row(row), p.Add(e.scroll.Offset))
  736. }
  737. return row, col
  738. }
  739. // pasteFromClipboard inserts text from the clipboard content,
  740. // starting from the cursor position.
  741. func (e *Entry) pasteFromClipboard(clipboard fyne.Clipboard) {
  742. if e.selecting {
  743. e.eraseSelection()
  744. }
  745. text := clipboard.Content()
  746. if !e.MultiLine {
  747. // format clipboard content to be compatible with single line entry
  748. text = strings.Replace(text, "\n", " ", -1)
  749. }
  750. provider := e.textProvider()
  751. runes := []rune(text)
  752. pos := e.cursorTextPos()
  753. provider.insertAt(pos, text)
  754. e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos + len(runes))
  755. e.updateText(provider.String())
  756. e.Refresh()
  757. }
  758. // placeholderProvider returns the placeholder text handler for this entry
  759. func (e *Entry) placeholderProvider() *RichText {
  760. if e.placeholder != nil {
  761. return e.placeholder
  762. }
  763. style := RichTextStyleInline
  764. style.ColorName = theme.ColorNamePlaceHolder
  765. text := NewRichText(&TextSegment{
  766. Style: style,
  767. Text: e.PlaceHolder,
  768. })
  769. text.ExtendBaseWidget(text)
  770. text.inset = fyne.NewSize(0, theme.InputBorderSize())
  771. e.placeholder = text
  772. return e.placeholder
  773. }
  774. func (e *Entry) registerShortcut() {
  775. e.shortcut.AddShortcut(&fyne.ShortcutCut{}, func(se fyne.Shortcut) {
  776. cut := se.(*fyne.ShortcutCut)
  777. e.cutToClipboard(cut.Clipboard)
  778. })
  779. e.shortcut.AddShortcut(&fyne.ShortcutCopy{}, func(se fyne.Shortcut) {
  780. cpy := se.(*fyne.ShortcutCopy)
  781. e.copyToClipboard(cpy.Clipboard)
  782. })
  783. e.shortcut.AddShortcut(&fyne.ShortcutPaste{}, func(se fyne.Shortcut) {
  784. paste := se.(*fyne.ShortcutPaste)
  785. e.pasteFromClipboard(paste.Clipboard)
  786. })
  787. e.shortcut.AddShortcut(&fyne.ShortcutSelectAll{}, func(se fyne.Shortcut) {
  788. e.selectAll()
  789. })
  790. }
  791. func (e *Entry) requestFocus() {
  792. impl := e.super()
  793. if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil {
  794. c.Focus(impl.(fyne.Focusable))
  795. }
  796. }
  797. // Obtains row,col from a given textual position
  798. // expects a read or write lock to be held by the caller
  799. func (e *Entry) rowColFromTextPos(pos int) (row int, col int) {
  800. provider := e.textProvider()
  801. canWrap := e.Wrapping == fyne.TextWrapBreak || e.Wrapping == fyne.TextWrapWord
  802. totalRows := provider.rows()
  803. for i := 0; i < totalRows; i++ {
  804. b := provider.rowBoundary(i)
  805. if b == nil {
  806. continue
  807. }
  808. if b.begin <= pos {
  809. if b.end < pos {
  810. row++
  811. }
  812. col = pos - b.begin
  813. // if this gap is at `pos` and is a line wrap, increment (safe to access boundary i-1)
  814. if canWrap && b.begin == pos && pos != 0 && provider.rowBoundary(i-1).end == b.begin && row < (totalRows-1) {
  815. row++
  816. }
  817. } else {
  818. break
  819. }
  820. }
  821. return
  822. }
  823. // selectAll selects all text in entry
  824. func (e *Entry) selectAll() {
  825. if e.textProvider().len() == 0 {
  826. return
  827. }
  828. e.setFieldsAndRefresh(func() {
  829. e.selectRow = 0
  830. e.selectColumn = 0
  831. lastRow := e.textProvider().rows() - 1
  832. e.CursorColumn = e.textProvider().rowLength(lastRow)
  833. e.CursorRow = lastRow
  834. e.selecting = true
  835. })
  836. }
  837. // selectingKeyHandler performs keypress action in the scenario that a selection
  838. // is either a) in progress or b) about to start
  839. // returns true if the keypress has been fully handled
  840. func (e *Entry) selectingKeyHandler(key *fyne.KeyEvent) bool {
  841. if e.selectKeyDown && !e.selecting {
  842. switch key.Name {
  843. case fyne.KeyUp, fyne.KeyDown,
  844. fyne.KeyLeft, fyne.KeyRight,
  845. fyne.KeyEnd, fyne.KeyHome,
  846. fyne.KeyPageUp, fyne.KeyPageDown:
  847. e.selecting = true
  848. }
  849. }
  850. if !e.selecting {
  851. return false
  852. }
  853. switch key.Name {
  854. case fyne.KeyBackspace, fyne.KeyDelete:
  855. // clears the selection -- return handled
  856. e.eraseSelection()
  857. return true
  858. case fyne.KeyReturn, fyne.KeyEnter:
  859. // clear the selection -- return unhandled to add the newline
  860. e.eraseSelection()
  861. return false
  862. }
  863. if !e.selectKeyDown {
  864. switch key.Name {
  865. case fyne.KeyLeft:
  866. // seek to the start of the selection -- return handled
  867. selectStart, _ := e.selection()
  868. e.propertyLock.Lock()
  869. e.CursorRow, e.CursorColumn = e.rowColFromTextPos(selectStart)
  870. e.selecting = false
  871. e.propertyLock.Unlock()
  872. return true
  873. case fyne.KeyRight:
  874. // seek to the end of the selection -- return handled
  875. _, selectEnd := e.selection()
  876. e.propertyLock.Lock()
  877. e.CursorRow, e.CursorColumn = e.rowColFromTextPos(selectEnd)
  878. e.selecting = false
  879. e.propertyLock.Unlock()
  880. return true
  881. case fyne.KeyUp, fyne.KeyDown, fyne.KeyEnd, fyne.KeyHome, fyne.KeyPageUp, fyne.KeyPageDown:
  882. // cursor movement without left or right shift -- clear selection and return unhandled
  883. e.selecting = false
  884. return false
  885. }
  886. }
  887. return false
  888. }
  889. // selection returns the start and end text positions for the selected span of text
  890. // Note: this functionality depends on the relationship between the selection start row/col and
  891. // the current cursor row/column.
  892. // eg: (whitespace for clarity, '_' denotes cursor)
  893. //
  894. // "T e s [t i]_n g" == 3, 5
  895. // "T e s_[t i] n g" == 3, 5
  896. // "T e_[s t i] n g" == 2, 5
  897. func (e *Entry) selection() (int, int) {
  898. e.propertyLock.RLock()
  899. noSelection := !e.selecting || (e.CursorRow == e.selectRow && e.CursorColumn == e.selectColumn)
  900. e.propertyLock.RUnlock()
  901. if noSelection {
  902. return -1, -1
  903. }
  904. e.propertyLock.Lock()
  905. defer e.propertyLock.Unlock()
  906. // Find the selection start
  907. rowA, colA := e.CursorRow, e.CursorColumn
  908. rowB, colB := e.selectRow, e.selectColumn
  909. // Reposition if the cursors row is more than select start row, or if the row is the same and
  910. // the cursors col is more that the select start column
  911. if rowA > e.selectRow || (rowA == e.selectRow && colA > e.selectColumn) {
  912. rowA, colA = e.selectRow, e.selectColumn
  913. rowB, colB = e.CursorRow, e.CursorColumn
  914. }
  915. return e.textPosFromRowCol(rowA, colA), e.textPosFromRowCol(rowB, colB)
  916. }
  917. // Obtains textual position from a given row and col
  918. // expects a read or write lock to be held by the caller
  919. func (e *Entry) textPosFromRowCol(row, col int) int {
  920. b := e.textProvider().rowBoundary(row)
  921. if b == nil {
  922. return col
  923. }
  924. return b.begin + col
  925. }
  926. func (e *Entry) syncSegments() {
  927. colName := theme.ColorNameForeground
  928. wrap := e.textWrap()
  929. if e.disabled {
  930. colName = theme.ColorNameDisabled
  931. }
  932. e.textProvider().Wrapping = wrap
  933. style := RichTextStyle{
  934. Alignment: fyne.TextAlignLeading,
  935. ColorName: colName,
  936. TextStyle: e.TextStyle,
  937. }
  938. if e.Password {
  939. style = RichTextStylePassword
  940. style.ColorName = colName
  941. style.TextStyle = e.TextStyle
  942. }
  943. e.textProvider().Segments = []RichTextSegment{&TextSegment{
  944. Style: style,
  945. Text: e.Text,
  946. }}
  947. colName = theme.ColorNamePlaceHolder
  948. if e.disabled {
  949. colName = theme.ColorNameDisabled
  950. }
  951. e.placeholderProvider().Wrapping = wrap
  952. e.placeholderProvider().Segments = []RichTextSegment{&TextSegment{
  953. Style: RichTextStyle{
  954. Alignment: fyne.TextAlignLeading,
  955. ColorName: colName,
  956. TextStyle: e.TextStyle,
  957. },
  958. Text: e.PlaceHolder,
  959. }}
  960. }
  961. // textProvider returns the text handler for this entry
  962. func (e *Entry) textProvider() *RichText {
  963. if e.text != nil {
  964. return e.text
  965. }
  966. if e.Text != "" {
  967. e.dirty = true
  968. }
  969. text := NewRichTextWithText(e.Text)
  970. text.ExtendBaseWidget(text)
  971. text.inset = fyne.NewSize(0, theme.InputBorderSize())
  972. e.text = text
  973. return e.text
  974. }
  975. // textWrap calculates the wrapping that we should apply.
  976. func (e *Entry) textWrap() fyne.TextWrap {
  977. if e.Wrapping == fyne.TextTruncate { // this is now the default - but we scroll around this large content
  978. return fyne.TextWrapOff
  979. }
  980. if !e.MultiLine && (e.Wrapping == fyne.TextWrapBreak || e.Wrapping == fyne.TextWrapWord) {
  981. fyne.LogError("Entry cannot wrap single line", nil)
  982. e.Wrapping = fyne.TextTruncate
  983. }
  984. return e.Wrapping
  985. }
  986. func (e *Entry) updateCursorAndSelection() {
  987. e.propertyLock.Lock()
  988. defer e.propertyLock.Unlock()
  989. e.CursorRow, e.CursorColumn = e.truncatePosition(e.CursorRow, e.CursorColumn)
  990. e.selectRow, e.selectColumn = e.truncatePosition(e.selectRow, e.selectColumn)
  991. }
  992. func (e *Entry) updateFromData(data binding.DataItem) {
  993. if data == nil {
  994. return
  995. }
  996. textSource, ok := data.(binding.String)
  997. if !ok {
  998. return
  999. }
  1000. val, err := textSource.Get()
  1001. e.conversionError = err
  1002. e.Validate()
  1003. if err != nil {
  1004. return
  1005. }
  1006. e.SetText(val)
  1007. }
  1008. func (e *Entry) truncatePosition(row, col int) (int, int) {
  1009. if e.Text == "" {
  1010. return 0, 0
  1011. }
  1012. newRow := row
  1013. newCol := col
  1014. if row >= e.textProvider().rows() {
  1015. newRow = e.textProvider().rows() - 1
  1016. }
  1017. rowLength := e.textProvider().rowLength(newRow)
  1018. if (newCol >= rowLength) || (newRow < row) {
  1019. newCol = rowLength
  1020. }
  1021. return newRow, newCol
  1022. }
  1023. func (e *Entry) updateMousePointer(p fyne.Position, rightClick bool) {
  1024. row, col := e.getRowCol(p)
  1025. e.propertyLock.Lock()
  1026. if !rightClick || !e.selecting {
  1027. e.CursorRow = row
  1028. e.CursorColumn = col
  1029. }
  1030. if !e.selecting {
  1031. e.selectRow = row
  1032. e.selectColumn = col
  1033. }
  1034. e.propertyLock.Unlock()
  1035. r := cache.Renderer(e.content)
  1036. if r != nil {
  1037. r.(*entryContentRenderer).moveCursor()
  1038. }
  1039. }
  1040. // updateText updates the internal text to the given value
  1041. func (e *Entry) updateText(text string) {
  1042. var callback func(string)
  1043. e.setFieldsAndRefresh(func() {
  1044. changed := e.Text != text
  1045. e.Text = text
  1046. e.syncSegments()
  1047. e.text.updateRowBounds()
  1048. if e.Text != "" {
  1049. e.dirty = true
  1050. }
  1051. if changed {
  1052. callback = e.OnChanged
  1053. }
  1054. })
  1055. e.Validate()
  1056. if callback != nil {
  1057. callback(text)
  1058. }
  1059. }
  1060. func (e *Entry) writeData(data binding.DataItem) {
  1061. if data == nil {
  1062. return
  1063. }
  1064. textTarget, ok := data.(binding.String)
  1065. if !ok {
  1066. return
  1067. }
  1068. curValue, err := textTarget.Get()
  1069. if err == nil && curValue == e.Text {
  1070. e.conversionError = nil
  1071. return
  1072. }
  1073. e.conversionError = textTarget.Set(e.Text)
  1074. }
  1075. func (e *Entry) typedKeyReturn(provider *RichText, multiLine bool) {
  1076. e.propertyLock.RLock()
  1077. onSubmitted := e.OnSubmitted
  1078. selectDown := e.selectKeyDown
  1079. text := e.Text
  1080. e.propertyLock.RUnlock()
  1081. if !multiLine {
  1082. // Single line doesn't support newline.
  1083. // Call submitted callback, if any.
  1084. if onSubmitted != nil {
  1085. onSubmitted(text)
  1086. }
  1087. return
  1088. } else if selectDown && onSubmitted != nil {
  1089. // Multiline supports newline, unless shift is held and OnSubmitted is set.
  1090. onSubmitted(text)
  1091. return
  1092. }
  1093. e.propertyLock.Lock()
  1094. provider.insertAt(e.cursorTextPos(), "\n")
  1095. e.CursorColumn = 0
  1096. e.CursorRow++
  1097. e.propertyLock.Unlock()
  1098. }
  1099. var _ fyne.WidgetRenderer = (*entryRenderer)(nil)
  1100. type entryRenderer struct {
  1101. box, border *canvas.Rectangle
  1102. scroll *widget.Scroll
  1103. objects []fyne.CanvasObject
  1104. entry *Entry
  1105. }
  1106. func (r *entryRenderer) Destroy() {
  1107. }
  1108. func (r *entryRenderer) trailingInset() float32 {
  1109. xInset := float32(0)
  1110. if r.entry.ActionItem != nil {
  1111. xInset = theme.IconInlineSize() + theme.LineSpacing()
  1112. }
  1113. if r.entry.Validator != nil {
  1114. if r.entry.ActionItem == nil {
  1115. xInset = theme.IconInlineSize() + theme.LineSpacing()
  1116. } else {
  1117. xInset += theme.IconInlineSize() + theme.LineSpacing()
  1118. }
  1119. }
  1120. return xInset
  1121. }
  1122. func (r *entryRenderer) Layout(size fyne.Size) {
  1123. // 0.5 is removed so on low DPI it rounds down on the trailing edge
  1124. r.border.Resize(fyne.NewSize(size.Width-theme.InputBorderSize()-.5, size.Height-theme.InputBorderSize()-.5))
  1125. r.border.StrokeWidth = theme.InputBorderSize()
  1126. r.border.Move(fyne.NewPos(theme.InputBorderSize()/2, theme.InputBorderSize()/2))
  1127. r.box.Resize(size.Subtract(fyne.NewSize(theme.InputBorderSize()*2, theme.InputBorderSize()*2)))
  1128. r.box.Move(fyne.NewPos(theme.InputBorderSize(), theme.InputBorderSize()))
  1129. actionIconSize := fyne.NewSize(0, 0)
  1130. if r.entry.ActionItem != nil {
  1131. actionIconSize = fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize())
  1132. r.entry.ActionItem.Resize(actionIconSize)
  1133. r.entry.ActionItem.Move(fyne.NewPos(size.Width-actionIconSize.Width-theme.InnerPadding(), theme.InnerPadding()))
  1134. }
  1135. validatorIconSize := fyne.NewSize(0, 0)
  1136. if r.entry.Validator != nil {
  1137. validatorIconSize = fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize())
  1138. r.ensureValidationSetup()
  1139. r.entry.validationStatus.Resize(validatorIconSize)
  1140. if r.entry.ActionItem == nil {
  1141. r.entry.validationStatus.Move(fyne.NewPos(size.Width-validatorIconSize.Width-theme.InnerPadding(), theme.InnerPadding()))
  1142. } else {
  1143. r.entry.validationStatus.Move(fyne.NewPos(size.Width-validatorIconSize.Width-actionIconSize.Width-theme.InnerPadding()-theme.LineSpacing(), theme.InnerPadding()))
  1144. }
  1145. }
  1146. r.entry.textProvider().inset = fyne.NewSize(0, theme.InputBorderSize())
  1147. r.entry.placeholderProvider().inset = fyne.NewSize(0, theme.InputBorderSize())
  1148. entrySize := size.Subtract(fyne.NewSize(r.trailingInset(), theme.InputBorderSize()*2))
  1149. entryPos := fyne.NewPos(0, theme.InputBorderSize())
  1150. r.entry.propertyLock.Lock()
  1151. textPos := r.entry.textPosFromRowCol(r.entry.CursorRow, r.entry.CursorColumn)
  1152. selectPos := r.entry.textPosFromRowCol(r.entry.selectRow, r.entry.selectColumn)
  1153. r.entry.propertyLock.Unlock()
  1154. if r.entry.Wrapping == fyne.TextWrapOff {
  1155. r.entry.content.Resize(entrySize)
  1156. r.entry.content.Move(entryPos)
  1157. } else {
  1158. r.scroll.Resize(entrySize)
  1159. r.scroll.Move(entryPos)
  1160. }
  1161. r.entry.propertyLock.Lock()
  1162. resizedTextPos := r.entry.textPosFromRowCol(r.entry.CursorRow, r.entry.CursorColumn)
  1163. r.entry.propertyLock.Unlock()
  1164. if textPos != resizedTextPos {
  1165. r.entry.setFieldsAndRefresh(func() {
  1166. r.entry.CursorRow, r.entry.CursorColumn = r.entry.rowColFromTextPos(textPos)
  1167. if r.entry.selecting {
  1168. r.entry.selectRow, r.entry.selectColumn = r.entry.rowColFromTextPos(selectPos)
  1169. }
  1170. })
  1171. }
  1172. }
  1173. // MinSize calculates the minimum size of an entry widget.
  1174. // This is based on the contained text with a standard amount of padding added.
  1175. // If MultiLine is true then we will reserve space for at leasts 3 lines
  1176. func (r *entryRenderer) MinSize() fyne.Size {
  1177. if r.scroll.Direction == widget.ScrollNone {
  1178. return r.entry.content.MinSize().Add(fyne.NewSize(0, theme.InputBorderSize()*2))
  1179. }
  1180. charMin := r.entry.placeholderProvider().charMinSize(r.entry.Password, r.entry.TextStyle)
  1181. minSize := charMin.Add(fyne.NewSize(theme.InnerPadding(), theme.InnerPadding()))
  1182. if r.entry.MultiLine {
  1183. count := r.entry.multiLineRows
  1184. if count <= 0 {
  1185. count = multiLineRows
  1186. }
  1187. // ensure multiline height is at least charMinSize * multilineRows
  1188. rowHeight := charMin.Height * float32(count)
  1189. minSize.Height = fyne.Max(minSize.Height, rowHeight+float32(count-1)*theme.LineSpacing())
  1190. }
  1191. return minSize.Add(fyne.NewSize(theme.InnerPadding()*2, theme.InnerPadding()))
  1192. }
  1193. func (r *entryRenderer) Objects() []fyne.CanvasObject {
  1194. r.entry.propertyLock.RLock()
  1195. defer r.entry.propertyLock.RUnlock()
  1196. return r.objects
  1197. }
  1198. func (r *entryRenderer) Refresh() {
  1199. r.entry.propertyLock.RLock()
  1200. content := r.entry.content
  1201. focusedAppearance := r.entry.focused && !r.entry.disabled
  1202. size := r.entry.size
  1203. wrapping := r.entry.Wrapping
  1204. r.entry.propertyLock.RUnlock()
  1205. r.entry.syncSegments()
  1206. r.entry.text.updateRowBounds()
  1207. r.entry.placeholder.updateRowBounds()
  1208. r.entry.text.Refresh()
  1209. r.entry.placeholder.Refresh()
  1210. // correct our scroll wrappers if the wrap mode changed
  1211. entrySize := size.Subtract(fyne.NewSize(r.trailingInset(), theme.InputBorderSize()*2))
  1212. if wrapping == fyne.TextWrapOff && r.scroll.Content != nil {
  1213. r.scroll.Hide()
  1214. r.scroll.Content = nil
  1215. content.Move(fyne.NewPos(0, theme.InputBorderSize()))
  1216. content.Resize(entrySize)
  1217. for i, o := range r.objects {
  1218. if o == r.scroll {
  1219. r.objects[i] = content
  1220. break
  1221. }
  1222. }
  1223. } else if wrapping != fyne.TextWrapOff && r.scroll.Content == nil {
  1224. r.scroll.Content = content
  1225. content.Move(fyne.NewPos(0, 0))
  1226. r.scroll.Move(fyne.NewPos(0, theme.InputBorderSize()))
  1227. r.scroll.Resize(entrySize)
  1228. r.scroll.Show()
  1229. for i, o := range r.objects {
  1230. if o == content {
  1231. r.objects[i] = r.scroll
  1232. break
  1233. }
  1234. }
  1235. }
  1236. r.entry.updateCursorAndSelection()
  1237. r.box.FillColor = theme.InputBackgroundColor()
  1238. if focusedAppearance {
  1239. r.border.StrokeColor = theme.PrimaryColor()
  1240. } else {
  1241. if r.entry.Disabled() {
  1242. r.border.StrokeColor = theme.DisabledColor()
  1243. } else {
  1244. r.border.StrokeColor = theme.InputBorderColor()
  1245. }
  1246. }
  1247. if r.entry.ActionItem != nil {
  1248. r.entry.ActionItem.Refresh()
  1249. }
  1250. if r.entry.Validator != nil {
  1251. if !r.entry.focused && !r.entry.Disabled() && r.entry.dirty && r.entry.validationError != nil {
  1252. r.border.StrokeColor = theme.ErrorColor()
  1253. }
  1254. r.ensureValidationSetup()
  1255. r.entry.validationStatus.Refresh()
  1256. } else if r.entry.validationStatus != nil {
  1257. r.entry.validationStatus.Hide()
  1258. }
  1259. cache.Renderer(r.entry.content).Refresh()
  1260. canvas.Refresh(r.entry.super())
  1261. }
  1262. func (r *entryRenderer) ensureValidationSetup() {
  1263. if r.entry.validationStatus == nil {
  1264. r.entry.validationStatus = newValidationStatus(r.entry)
  1265. r.objects = append(r.objects, r.entry.validationStatus)
  1266. r.Layout(r.entry.size)
  1267. r.entry.Validate()
  1268. r.Refresh()
  1269. }
  1270. }
  1271. var _ fyne.Widget = (*entryContent)(nil)
  1272. type entryContent struct {
  1273. BaseWidget
  1274. entry *Entry
  1275. scroll *widget.Scroll
  1276. }
  1277. func (e *entryContent) CreateRenderer() fyne.WidgetRenderer {
  1278. e.ExtendBaseWidget(e)
  1279. e.entry.propertyLock.Lock()
  1280. defer e.entry.propertyLock.Unlock()
  1281. provider := e.entry.textProvider()
  1282. placeholder := e.entry.placeholderProvider()
  1283. if provider.len() != 0 {
  1284. placeholder.Hide()
  1285. }
  1286. objects := []fyne.CanvasObject{placeholder, provider, e.entry.cursorAnim.cursor}
  1287. r := &entryContentRenderer{e.entry.cursorAnim.cursor, []fyne.CanvasObject{}, objects,
  1288. provider, placeholder, e}
  1289. r.updateScrollDirections()
  1290. r.Layout(e.size)
  1291. return r
  1292. }
  1293. // DragEnd is called at end of a drag event.
  1294. //
  1295. // Implements: fyne.Draggable
  1296. func (e *entryContent) DragEnd() {
  1297. // we need to propagate the focus, top level widget handles focus APIs
  1298. e.entry.requestFocus()
  1299. e.entry.DragEnd()
  1300. }
  1301. // Dragged is called when the pointer moves while a button is held down.
  1302. // It updates the selection accordingly.
  1303. //
  1304. // Implements: fyne.Draggable
  1305. func (e *entryContent) Dragged(d *fyne.DragEvent) {
  1306. e.entry.Dragged(d)
  1307. }
  1308. var _ fyne.WidgetRenderer = (*entryContentRenderer)(nil)
  1309. type entryContentRenderer struct {
  1310. cursor *canvas.Rectangle
  1311. selection []fyne.CanvasObject
  1312. objects []fyne.CanvasObject
  1313. provider, placeholder *RichText
  1314. content *entryContent
  1315. }
  1316. func (r *entryContentRenderer) Destroy() {
  1317. r.content.entry.cursorAnim.stop()
  1318. }
  1319. func (r *entryContentRenderer) Layout(size fyne.Size) {
  1320. r.provider.Resize(size)
  1321. r.placeholder.Resize(size)
  1322. }
  1323. func (r *entryContentRenderer) MinSize() fyne.Size {
  1324. minSize := r.content.entry.placeholderProvider().MinSize()
  1325. if r.content.entry.textProvider().len() > 0 {
  1326. minSize = r.content.entry.text.MinSize()
  1327. }
  1328. return minSize
  1329. }
  1330. func (r *entryContentRenderer) Objects() []fyne.CanvasObject {
  1331. r.content.entry.propertyLock.RLock()
  1332. defer r.content.entry.propertyLock.RUnlock()
  1333. // Objects are generated dynamically force selection rectangles to appear underneath the text
  1334. if r.content.entry.selecting {
  1335. objs := make([]fyne.CanvasObject, 0, len(r.selection)+len(r.objects))
  1336. objs = append(objs, r.selection...)
  1337. return append(objs, r.objects...)
  1338. }
  1339. return r.objects
  1340. }
  1341. func (r *entryContentRenderer) Refresh() {
  1342. r.content.entry.propertyLock.RLock()
  1343. provider := r.content.entry.textProvider()
  1344. placeholder := r.content.entry.placeholderProvider()
  1345. focusedAppearance := r.content.entry.focused && !r.content.entry.disabled
  1346. selections := r.selection
  1347. r.updateScrollDirections()
  1348. r.content.entry.propertyLock.RUnlock()
  1349. if provider.len() == 0 {
  1350. placeholder.Show()
  1351. } else if placeholder.Visible() {
  1352. placeholder.Hide()
  1353. }
  1354. if focusedAppearance {
  1355. r.cursor.Show()
  1356. r.content.entry.cursorAnim.start()
  1357. } else {
  1358. r.content.entry.cursorAnim.stop()
  1359. r.cursor.Hide()
  1360. }
  1361. r.moveCursor()
  1362. for _, selection := range selections {
  1363. selection.(*canvas.Rectangle).Hidden = !r.content.entry.focused
  1364. selection.(*canvas.Rectangle).FillColor = theme.SelectionColor()
  1365. }
  1366. canvas.Refresh(r.content)
  1367. }
  1368. // This process builds a slice of rectangles:
  1369. // - one entry per row of text
  1370. // - ordered by row order as they occur in multiline text
  1371. // This process could be optimized in the scenario where the user is selecting upwards:
  1372. // If the upwards case instead produces an order-reversed slice then only the newest rectangle would
  1373. // require movement and resizing. The existing solution creates a new rectangle and then moves/resizes
  1374. // all rectangles to comply with the occurrence order as stated above.
  1375. func (r *entryContentRenderer) buildSelection() {
  1376. r.content.entry.propertyLock.RLock()
  1377. cursorRow, cursorCol := r.content.entry.CursorRow, r.content.entry.CursorColumn
  1378. selectRow, selectCol := -1, -1
  1379. if r.content.entry.selecting {
  1380. selectRow = r.content.entry.selectRow
  1381. selectCol = r.content.entry.selectColumn
  1382. }
  1383. r.content.entry.propertyLock.RUnlock()
  1384. if selectRow == -1 || (cursorRow == selectRow && cursorCol == selectCol) {
  1385. r.selection = r.selection[:0]
  1386. return
  1387. }
  1388. provider := r.content.entry.textProvider()
  1389. // Convert column, row into x,y
  1390. getCoordinates := func(column int, row int) (float32, float32) {
  1391. sz := provider.lineSizeToColumn(column, row)
  1392. return sz.Width, sz.Height*float32(row) - theme.InputBorderSize() + theme.InnerPadding()
  1393. }
  1394. lineHeight := r.content.entry.text.charMinSize(r.content.entry.Password, r.content.entry.TextStyle).Height
  1395. minmax := func(a, b int) (int, int) {
  1396. if a < b {
  1397. return a, b
  1398. }
  1399. return b, a
  1400. }
  1401. // The remainder of the function calculates the set of boxes and add them to r.selection
  1402. selectStartRow, selectEndRow := minmax(selectRow, cursorRow)
  1403. selectStartCol, selectEndCol := minmax(selectCol, cursorCol)
  1404. if selectRow < cursorRow {
  1405. selectStartCol, selectEndCol = selectCol, cursorCol
  1406. }
  1407. if selectRow > cursorRow {
  1408. selectStartCol, selectEndCol = cursorCol, selectCol
  1409. }
  1410. rowCount := selectEndRow - selectStartRow + 1
  1411. // trim r.selection to remove unwanted old rectangles
  1412. if len(r.selection) > rowCount {
  1413. r.selection = r.selection[:rowCount]
  1414. }
  1415. r.content.entry.propertyLock.Lock()
  1416. defer r.content.entry.propertyLock.Unlock()
  1417. // build a rectangle for each row and add it to r.selection
  1418. for i := 0; i < rowCount; i++ {
  1419. if len(r.selection) <= i {
  1420. box := canvas.NewRectangle(theme.SelectionColor())
  1421. r.selection = append(r.selection, box)
  1422. }
  1423. // determine starting/ending columns for this rectangle
  1424. row := selectStartRow + i
  1425. startCol, endCol := selectStartCol, selectEndCol
  1426. if selectStartRow < row {
  1427. startCol = 0
  1428. }
  1429. if selectEndRow > row {
  1430. endCol = provider.rowLength(row)
  1431. }
  1432. // translate columns and row into draw coordinates
  1433. x1, y1 := getCoordinates(startCol, row)
  1434. x2, _ := getCoordinates(endCol, row)
  1435. // resize and reposition each rectangle
  1436. r.selection[i].Resize(fyne.NewSize(x2-x1+1, lineHeight))
  1437. r.selection[i].Move(fyne.NewPos(x1-1, y1))
  1438. }
  1439. }
  1440. func (r *entryContentRenderer) ensureCursorVisible() {
  1441. letter := fyne.MeasureText("e", theme.TextSize(), r.content.entry.TextStyle)
  1442. padX := letter.Width*2 + theme.LineSpacing()
  1443. padY := letter.Height - theme.LineSpacing()
  1444. cx := r.cursor.Position().X
  1445. cy := r.cursor.Position().Y
  1446. cx1 := cx - padX
  1447. cy1 := cy - padY
  1448. cx2 := cx + r.cursor.Size().Width + padX
  1449. cy2 := cy + r.cursor.Size().Height + padY
  1450. offset := r.content.scroll.Offset
  1451. size := r.content.scroll.Size()
  1452. if offset.X <= cx1 && cx2 < offset.X+size.Width &&
  1453. offset.Y <= cy1 && cy2 < offset.Y+size.Height {
  1454. return
  1455. }
  1456. move := fyne.NewDelta(0, 0)
  1457. if cx1 < offset.X {
  1458. move.DX -= offset.X - cx1
  1459. } else if cx2 >= offset.X+size.Width {
  1460. move.DX += cx2 - (offset.X + size.Width)
  1461. }
  1462. if cy1 < offset.Y {
  1463. move.DY -= offset.Y - cy1
  1464. } else if cy2 >= offset.Y+size.Height {
  1465. move.DY += cy2 - (offset.Y + size.Height)
  1466. }
  1467. if r.content.scroll.Content != nil {
  1468. r.content.scroll.Offset = r.content.scroll.Offset.Add(move)
  1469. r.content.scroll.Refresh()
  1470. }
  1471. }
  1472. func (r *entryContentRenderer) moveCursor() {
  1473. // build r.selection[] if the user has made a selection
  1474. r.buildSelection()
  1475. r.content.entry.propertyLock.RLock()
  1476. provider := r.content.entry.textProvider()
  1477. provider.propertyLock.RLock()
  1478. size := provider.lineSizeToColumn(r.content.entry.CursorColumn, r.content.entry.CursorRow)
  1479. provider.propertyLock.RUnlock()
  1480. xPos := size.Width
  1481. yPos := size.Height * float32(r.content.entry.CursorRow)
  1482. r.content.entry.propertyLock.RUnlock()
  1483. r.content.entry.propertyLock.Lock()
  1484. lineHeight := r.content.entry.text.charMinSize(r.content.entry.Password, r.content.entry.TextStyle).Height
  1485. r.cursor.Resize(fyne.NewSize(theme.InputBorderSize(), lineHeight))
  1486. r.cursor.Move(fyne.NewPos(xPos-(theme.InputBorderSize()/2), yPos+theme.InnerPadding()-theme.InputBorderSize()))
  1487. callback := r.content.entry.OnCursorChanged
  1488. r.content.entry.propertyLock.Unlock()
  1489. r.ensureCursorVisible()
  1490. if callback != nil {
  1491. callback()
  1492. }
  1493. }
  1494. func (r *entryContentRenderer) updateScrollDirections() {
  1495. if r.content.scroll == nil { // not scrolling
  1496. return
  1497. }
  1498. switch r.content.entry.Wrapping {
  1499. case fyne.TextWrapOff:
  1500. r.content.scroll.Direction = widget.ScrollNone
  1501. case fyne.TextTruncate: // this is now the default - but we scroll
  1502. r.content.scroll.Direction = widget.ScrollBoth
  1503. default: // fyne.TextWrapBreak, fyne.TextWrapWord
  1504. r.content.scroll.Direction = widget.ScrollVerticalOnly
  1505. }
  1506. }
  1507. // getTextWhitespaceRegion returns the start/end markers for selection highlight on starting from col
  1508. // and expanding to the start and end of the whitespace or text underneath the specified position.
  1509. func getTextWhitespaceRegion(row []rune, col int) (int, int) {
  1510. if len(row) == 0 || col < 0 {
  1511. return -1, -1
  1512. }
  1513. // If the click position exceeds the length of text then snap it to the end
  1514. if col >= len(row) {
  1515. col = len(row) - 1
  1516. }
  1517. // maps: " fi-sh 日本語本語日 \t "
  1518. // into: " -- -- ------ "
  1519. space := func(r rune) rune {
  1520. if unicode.IsSpace(r) {
  1521. return ' '
  1522. }
  1523. // If this rune is a typical word separator then classify it as whitespace
  1524. if strings.ContainsRune(doubleClickWordSeperator, r) {
  1525. return ' '
  1526. }
  1527. return '-'
  1528. }
  1529. toks := strings.Map(space, string(row))
  1530. c := byte(' ')
  1531. if toks[col] == ' ' {
  1532. c = byte('-')
  1533. }
  1534. // LastIndexByte + 1 ensures that the position of the unwanted character 'c' is excluded
  1535. // +1 also has the added side effect whereby if 'c' isn't found then -1 is snapped to 0
  1536. start := strings.LastIndexByte(toks[:col], c) + 1
  1537. // IndexByte will find the position of the next unwanted character, this is to be the end
  1538. // marker for the selection
  1539. end := strings.IndexByte(toks[col:], c)
  1540. if end == -1 {
  1541. end = len(toks) // snap end to len(toks) if it results in -1
  1542. } else {
  1543. end += col // otherwise include the text slice position
  1544. }
  1545. return start, end
  1546. }