richtext.go 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106
  1. package widget
  2. import (
  3. "image/color"
  4. "math"
  5. "strings"
  6. "sync"
  7. "unicode"
  8. "fyne.io/fyne/v2"
  9. "fyne.io/fyne/v2/canvas"
  10. "fyne.io/fyne/v2/internal/cache"
  11. paint "fyne.io/fyne/v2/internal/painter"
  12. "fyne.io/fyne/v2/internal/widget"
  13. "fyne.io/fyne/v2/layout"
  14. "fyne.io/fyne/v2/theme"
  15. "github.com/go-text/typesetting/di"
  16. "github.com/go-text/typesetting/shaping"
  17. "golang.org/x/image/math/fixed"
  18. )
  19. const (
  20. passwordChar = "•"
  21. )
  22. // RichText represents the base element for a rich text-based widget.
  23. //
  24. // Since: 2.1
  25. type RichText struct {
  26. BaseWidget
  27. Segments []RichTextSegment
  28. Wrapping fyne.TextWrap
  29. Scroll widget.ScrollDirection
  30. Truncation fyne.TextTruncation
  31. inset fyne.Size // this varies due to how the widget works (entry with scroller vs others with padding)
  32. rowBounds []rowBoundary // cache for boundaries
  33. scr *widget.Scroll
  34. prop *canvas.Rectangle // used to apply text minsize to the scroller `scr`, if present - TODO improve #2464
  35. visualCache map[RichTextSegment][]fyne.CanvasObject
  36. cacheLock sync.Mutex
  37. minCache fyne.Size
  38. }
  39. // NewRichText returns a new RichText widget that renders the given text and segments.
  40. // If no segments are specified it will be converted to a single segment using the default text settings.
  41. //
  42. // Since: 2.1
  43. func NewRichText(segments ...RichTextSegment) *RichText {
  44. t := &RichText{Segments: segments}
  45. t.Scroll = widget.ScrollNone
  46. t.updateRowBounds()
  47. return t
  48. }
  49. // NewRichTextWithText returns a new RichText widget that renders the given text.
  50. // The string will be converted to a single text segment using the default text settings.
  51. //
  52. // Since: 2.1
  53. func NewRichTextWithText(text string) *RichText {
  54. return NewRichText(&TextSegment{
  55. Style: RichTextStyleInline,
  56. Text: text,
  57. })
  58. }
  59. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  60. func (t *RichText) CreateRenderer() fyne.WidgetRenderer {
  61. t.prop = canvas.NewRectangle(color.Transparent)
  62. if t.scr == nil && t.Scroll != widget.ScrollNone {
  63. t.scr = widget.NewScroll(&fyne.Container{Layout: layout.NewStackLayout(), Objects: []fyne.CanvasObject{
  64. t.prop, &fyne.Container{}}})
  65. }
  66. t.ExtendBaseWidget(t)
  67. r := &textRenderer{obj: t}
  68. t.updateRowBounds() // set up the initial text layout etc
  69. r.Refresh()
  70. return r
  71. }
  72. // MinSize calculates the minimum size of a rich text widget.
  73. // This is based on the contained text with a standard amount of padding added.
  74. func (t *RichText) MinSize() fyne.Size {
  75. // we don't return the minCache here, as any internal segments could have caused it to change...
  76. t.ExtendBaseWidget(t)
  77. min := t.BaseWidget.MinSize()
  78. t.minCache = min
  79. return min
  80. }
  81. // Refresh triggers a redraw of the rich text.
  82. //
  83. // Implements: fyne.Widget
  84. func (t *RichText) Refresh() {
  85. t.minCache = fyne.Size{}
  86. t.updateRowBounds()
  87. t.BaseWidget.Refresh()
  88. }
  89. // Resize sets a new size for the rich text.
  90. // This should only be called if it is not in a container with a layout manager.
  91. //
  92. // Implements: fyne.Widget
  93. func (t *RichText) Resize(size fyne.Size) {
  94. t.propertyLock.RLock()
  95. baseSize := t.size
  96. segments := t.Segments
  97. skipResize := !t.minCache.IsZero() && size.Width >= t.minCache.Width && size.Height >= t.minCache.Height && t.Wrapping == fyne.TextWrapOff && t.Truncation == fyne.TextTruncateOff
  98. t.propertyLock.RUnlock()
  99. if baseSize == size {
  100. return
  101. }
  102. t.propertyLock.Lock()
  103. t.size = size
  104. t.propertyLock.Unlock()
  105. if skipResize {
  106. if len(segments) < 2 { // we can simplify :)
  107. cache.Renderer(t).Layout(size)
  108. return
  109. }
  110. }
  111. t.updateRowBounds()
  112. t.Refresh()
  113. }
  114. // String returns the text widget buffer as string
  115. func (t *RichText) String() string {
  116. ret := strings.Builder{}
  117. for _, seg := range t.Segments {
  118. ret.WriteString(seg.Textual())
  119. }
  120. return ret.String()
  121. }
  122. // CharMinSize returns the average char size to use for internal computation
  123. func (t *RichText) charMinSize(concealed bool, style fyne.TextStyle) fyne.Size {
  124. defaultChar := "M"
  125. if concealed {
  126. defaultChar = passwordChar
  127. }
  128. return fyne.MeasureText(defaultChar, theme.TextSize(), style)
  129. }
  130. // deleteFromTo removes the text between the specified positions
  131. func (t *RichText) deleteFromTo(lowBound int, highBound int) string {
  132. start := 0
  133. var ret []rune
  134. deleting := false
  135. var segs []RichTextSegment
  136. for i, seg := range t.Segments {
  137. if _, ok := seg.(*TextSegment); !ok {
  138. if !deleting {
  139. segs = append(segs, seg)
  140. }
  141. continue
  142. }
  143. end := start + len([]rune(seg.(*TextSegment).Text))
  144. if end < lowBound {
  145. segs = append(segs, seg)
  146. start = end
  147. continue
  148. }
  149. startOff := int(math.Max(float64(lowBound-start), 0))
  150. endOff := int(math.Min(float64(end), float64(highBound))) - start
  151. deleted := make([]rune, endOff-startOff)
  152. r := ([]rune)(seg.(*TextSegment).Text)
  153. copy(deleted, r[startOff:endOff])
  154. ret = append(ret, deleted...)
  155. r2 := append(r[:startOff], r[endOff:]...)
  156. seg.(*TextSegment).Text = string(r2)
  157. segs = append(segs, seg)
  158. // prepare next iteration
  159. start = end
  160. if start >= highBound {
  161. segs = append(segs, t.Segments[i+1:]...)
  162. break
  163. } else if start >= lowBound {
  164. deleting = true
  165. }
  166. }
  167. t.Segments = segs
  168. t.Refresh()
  169. return string(ret)
  170. }
  171. // cachedSegmentVisual returns a cached segment visual representation.
  172. // The offset value is > 0 if the segment had been split and so we need multiple objects.
  173. func (t *RichText) cachedSegmentVisual(seg RichTextSegment, offset int) fyne.CanvasObject {
  174. t.cacheLock.Lock()
  175. defer t.cacheLock.Unlock()
  176. if t.visualCache == nil {
  177. t.visualCache = make(map[RichTextSegment][]fyne.CanvasObject)
  178. }
  179. if vis, ok := t.visualCache[seg]; ok && offset < len(vis) {
  180. return vis[offset]
  181. }
  182. vis := seg.Visual()
  183. if offset < len(t.visualCache[seg]) {
  184. t.visualCache[seg][offset] = vis
  185. } else {
  186. t.visualCache[seg] = append(t.visualCache[seg], vis)
  187. }
  188. return vis
  189. }
  190. // insertAt inserts the text at the specified position
  191. func (t *RichText) insertAt(pos int, runes string) {
  192. index := 0
  193. start := 0
  194. var into *TextSegment
  195. for i, seg := range t.Segments {
  196. if _, ok := seg.(*TextSegment); !ok {
  197. continue
  198. }
  199. end := start + len([]rune(seg.(*TextSegment).Text))
  200. into = seg.(*TextSegment)
  201. index = i
  202. if end > pos {
  203. break
  204. }
  205. start = end
  206. }
  207. if into == nil {
  208. return
  209. }
  210. r := ([]rune)(into.Text)
  211. if pos > len(r) { // safety in case position is out of bounds for the segment
  212. pos = len(r)
  213. }
  214. r2 := append(r[:pos], append([]rune(runes), r[pos:]...)...)
  215. into.Text = string(r2)
  216. t.Segments[index] = into
  217. }
  218. // Len returns the text widget buffer length
  219. func (t *RichText) len() int {
  220. ret := 0
  221. for _, seg := range t.Segments {
  222. ret += len([]rune(seg.Textual()))
  223. }
  224. return ret
  225. }
  226. // lineSizeToColumn returns the rendered size for the line specified by row up to the col position
  227. func (t *RichText) lineSizeToColumn(col, row int) fyne.Size {
  228. if row < 0 {
  229. row = 0
  230. }
  231. if col < 0 {
  232. col = 0
  233. }
  234. bound := t.rowBoundary(row)
  235. total := fyne.NewSize(0, 0)
  236. counted := 0
  237. last := false
  238. if bound == nil {
  239. return t.charMinSize(false, fyne.TextStyle{})
  240. }
  241. for i, seg := range bound.segments {
  242. var size fyne.Size
  243. if text, ok := seg.(*TextSegment); ok {
  244. start := 0
  245. if i == 0 {
  246. start = bound.begin
  247. }
  248. measureText := []rune(text.Text)[start:]
  249. if col < counted+len(measureText) {
  250. measureText = measureText[0 : col-counted]
  251. last = true
  252. }
  253. if concealed(seg) {
  254. measureText = []rune(strings.Repeat(passwordChar, len(measureText)))
  255. }
  256. counted += len(measureText)
  257. label := canvas.NewText(string(measureText), color.Black)
  258. label.TextStyle = text.Style.TextStyle
  259. label.TextSize = text.size()
  260. size = label.MinSize()
  261. } else {
  262. size = t.cachedSegmentVisual(seg, 0).MinSize()
  263. }
  264. total.Width += size.Width
  265. total.Height = fyne.Max(total.Height, size.Height)
  266. if last {
  267. break
  268. }
  269. }
  270. return total.Add(fyne.NewSize(theme.InnerPadding()-t.inset.Width, 0))
  271. }
  272. // Row returns the characters in the row specified.
  273. // The row parameter should be between 0 and t.Rows()-1.
  274. func (t *RichText) row(row int) []rune {
  275. if row < 0 || row >= t.rows() {
  276. return nil
  277. }
  278. bound := t.rowBounds[row]
  279. var ret []rune
  280. for i, seg := range bound.segments {
  281. if text, ok := seg.(*TextSegment); ok {
  282. if i == 0 {
  283. if len(bound.segments) == 1 {
  284. ret = append(ret, []rune(text.Text)[bound.begin:bound.end]...)
  285. } else {
  286. ret = append(ret, []rune(text.Text)[bound.begin:]...)
  287. }
  288. } else if i == len(bound.segments)-1 && len(bound.segments) > 1 && bound.end != 0 {
  289. ret = append(ret, []rune(text.Text)[:bound.end]...)
  290. }
  291. }
  292. }
  293. return ret
  294. }
  295. // RowBoundary returns the boundary of the row specified.
  296. // The row parameter should be between 0 and t.Rows()-1.
  297. func (t *RichText) rowBoundary(row int) *rowBoundary {
  298. t.propertyLock.RLock()
  299. defer t.propertyLock.RUnlock()
  300. if row < 0 || row >= t.rows() {
  301. return nil
  302. }
  303. return &t.rowBounds[row]
  304. }
  305. // RowLength returns the number of visible characters in the row specified.
  306. // The row parameter should be between 0 and t.Rows()-1.
  307. func (t *RichText) rowLength(row int) int {
  308. return len(t.row(row))
  309. }
  310. // rows returns the number of text rows in this text entry.
  311. // The entry may be longer than required to show this amount of content.
  312. func (t *RichText) rows() int {
  313. return len(t.rowBounds)
  314. }
  315. // updateRowBounds updates the row bounds used to render properly the text widget.
  316. // updateRowBounds should be invoked every time a segment Text, widget Wrapping or size changes.
  317. func (t *RichText) updateRowBounds() {
  318. innerPadding := theme.InnerPadding()
  319. fitSize := t.Size()
  320. if t.scr != nil {
  321. fitSize = t.scr.Content.MinSize()
  322. }
  323. fitSize.Height -= (innerPadding + t.inset.Height) * 2
  324. t.propertyLock.RLock()
  325. var bounds []rowBoundary
  326. maxWidth := t.size.Width - 2*innerPadding + 2*t.inset.Width
  327. wrapWidth := maxWidth
  328. var currentBound *rowBoundary
  329. var iterateSegments func(segList []RichTextSegment)
  330. iterateSegments = func(segList []RichTextSegment) {
  331. for _, seg := range segList {
  332. if parent, ok := seg.(RichTextBlock); ok {
  333. segs := parent.Segments()
  334. iterateSegments(segs)
  335. if len(segs) > 0 && !segs[len(segs)-1].Inline() {
  336. wrapWidth = maxWidth
  337. currentBound = nil
  338. }
  339. continue
  340. }
  341. if _, ok := seg.(*TextSegment); !ok {
  342. if currentBound == nil {
  343. bound := rowBoundary{segments: []RichTextSegment{seg}}
  344. bounds = append(bounds, bound)
  345. currentBound = &bound
  346. } else {
  347. bounds[len(bounds)-1].segments = append(bounds[len(bounds)-1].segments, seg)
  348. }
  349. itemMin := t.cachedSegmentVisual(seg, 0).MinSize()
  350. if seg.Inline() {
  351. wrapWidth -= itemMin.Width
  352. } else {
  353. wrapWidth = maxWidth
  354. currentBound = nil
  355. fitSize.Height -= itemMin.Height + theme.LineSpacing()
  356. }
  357. continue
  358. }
  359. textSeg := seg.(*TextSegment)
  360. textStyle := textSeg.Style.TextStyle
  361. textSize := textSeg.size()
  362. leftPad := float32(0)
  363. if textSeg.Style == RichTextStyleBlockquote {
  364. leftPad = innerPadding * 2
  365. }
  366. retBounds, height := lineBounds(textSeg, t.Wrapping, t.Truncation, wrapWidth-leftPad, fyne.NewSize(maxWidth, fitSize.Height), func(text []rune) fyne.Size {
  367. return fyne.MeasureText(string(text), textSize, textStyle)
  368. })
  369. if currentBound != nil {
  370. if len(retBounds) > 0 {
  371. bounds[len(bounds)-1].end = retBounds[0].end // invalidate row ending as we have more content
  372. bounds[len(bounds)-1].segments = append(bounds[len(bounds)-1].segments, seg)
  373. bounds = append(bounds, retBounds[1:]...)
  374. fitSize.Height -= height
  375. }
  376. } else {
  377. bounds = append(bounds, retBounds...)
  378. fitSize.Height -= height
  379. }
  380. currentBound = &bounds[len(bounds)-1]
  381. if seg.Inline() {
  382. last := bounds[len(bounds)-1]
  383. begin := 0
  384. if len(last.segments) == 1 {
  385. begin = last.begin
  386. }
  387. runes := []rune(textSeg.Text)
  388. // check ranges - as we resize it can be wrong?
  389. if begin > len(runes) {
  390. begin = len(runes)
  391. }
  392. end := last.end
  393. if end > len(runes) {
  394. end = len(runes)
  395. }
  396. text := string(runes[begin:end])
  397. measured := fyne.MeasureText(text, textSeg.size(), textSeg.Style.TextStyle)
  398. lastWidth := measured.Width
  399. if len(retBounds) == 1 {
  400. wrapWidth -= lastWidth
  401. } else {
  402. wrapWidth = maxWidth - lastWidth
  403. }
  404. } else {
  405. currentBound = nil
  406. wrapWidth = maxWidth
  407. }
  408. }
  409. }
  410. iterateSegments(t.Segments)
  411. t.propertyLock.RUnlock()
  412. t.propertyLock.Lock()
  413. t.rowBounds = bounds
  414. t.propertyLock.Unlock()
  415. }
  416. // RichTextBlock is an extension of a text segment that contains other segments
  417. //
  418. // Since: 2.1
  419. type RichTextBlock interface {
  420. Segments() []RichTextSegment
  421. }
  422. // Renderer
  423. type textRenderer struct {
  424. widget.BaseRenderer
  425. obj *RichText
  426. }
  427. func (r *textRenderer) Layout(size fyne.Size) {
  428. r.obj.propertyLock.RLock()
  429. bounds := r.obj.rowBounds
  430. objs := r.Objects()
  431. if r.obj.scr != nil {
  432. r.obj.scr.Resize(size)
  433. objs = r.obj.scr.Content.(*fyne.Container).Objects[1].(*fyne.Container).Objects
  434. }
  435. r.obj.propertyLock.RUnlock()
  436. // Accessing theme here is slow, so we cache the value
  437. innerPadding := theme.InnerPadding()
  438. lineSpacing := theme.LineSpacing()
  439. xInset := innerPadding - r.obj.inset.Width
  440. left := xInset
  441. yPos := innerPadding - r.obj.inset.Height
  442. lineWidth := size.Width - left*2
  443. var rowItems []fyne.CanvasObject
  444. rowAlign := fyne.TextAlignLeading
  445. i := 0
  446. for row, bound := range bounds {
  447. for segI := range bound.segments {
  448. if i == len(objs) {
  449. break // Refresh may not have created all objects for all rows yet...
  450. }
  451. inline := segI < len(bound.segments)-1
  452. obj := objs[i]
  453. i++
  454. _, isText := obj.(*canvas.Text)
  455. if !isText && !inline {
  456. if len(rowItems) != 0 {
  457. width, _ := r.layoutRow(rowItems, rowAlign, left, yPos, lineWidth)
  458. left += width
  459. rowItems = nil
  460. }
  461. height := obj.MinSize().Height
  462. obj.Move(fyne.NewPos(left, yPos))
  463. obj.Resize(fyne.NewSize(lineWidth, height))
  464. yPos += height
  465. left = xInset
  466. continue
  467. }
  468. rowItems = append(rowItems, obj)
  469. if inline {
  470. continue
  471. }
  472. leftPad := float32(0)
  473. if text, ok := bound.segments[0].(*TextSegment); ok {
  474. rowAlign = text.Style.Alignment
  475. if text.Style == RichTextStyleBlockquote {
  476. leftPad = lineSpacing * 4
  477. }
  478. } else if link, ok := bound.segments[0].(*HyperlinkSegment); ok {
  479. rowAlign = link.Alignment
  480. }
  481. _, y := r.layoutRow(rowItems, rowAlign, left+leftPad, yPos, lineWidth-leftPad)
  482. yPos += y
  483. rowItems = nil
  484. }
  485. lastSeg := bound.segments[len(bound.segments)-1]
  486. if !lastSeg.Inline() && row < len(bounds)-1 && bounds[row+1].segments[0] != lastSeg { // ignore wrapped lines etc
  487. yPos += lineSpacing
  488. }
  489. }
  490. }
  491. // MinSize calculates the minimum size of a rich text widget.
  492. // This is based on the contained text with a standard amount of padding added.
  493. func (r *textRenderer) MinSize() fyne.Size {
  494. r.obj.propertyLock.RLock()
  495. bounds := r.obj.rowBounds
  496. wrap := r.obj.Wrapping
  497. trunc := r.obj.Truncation
  498. scroll := r.obj.Scroll
  499. objs := r.Objects()
  500. if r.obj.scr != nil {
  501. objs = r.obj.scr.Content.(*fyne.Container).Objects[1].(*fyne.Container).Objects
  502. }
  503. r.obj.propertyLock.RUnlock()
  504. charMinSize := r.obj.charMinSize(false, fyne.TextStyle{})
  505. min := r.calculateMin(bounds, wrap, objs, charMinSize)
  506. if r.obj.scr != nil {
  507. r.obj.prop.SetMinSize(min)
  508. }
  509. if trunc != fyne.TextTruncateOff && r.obj.Scroll == widget.ScrollNone {
  510. minBounds := charMinSize
  511. if wrap == fyne.TextWrapOff {
  512. minBounds.Height = min.Height
  513. } else {
  514. minBounds = minBounds.Add(fyne.NewSquareSize(theme.InnerPadding() * 2).Subtract(r.obj.inset).Subtract(r.obj.inset))
  515. }
  516. if trunc == fyne.TextTruncateClip {
  517. return minBounds
  518. } else if trunc == fyne.TextTruncateEllipsis {
  519. ellipsisSize := fyne.MeasureText("…", theme.TextSize(), fyne.TextStyle{})
  520. return minBounds.AddWidthHeight(ellipsisSize.Width, 0)
  521. }
  522. }
  523. switch scroll {
  524. case widget.ScrollBoth:
  525. return fyne.NewSize(32, 32)
  526. case widget.ScrollHorizontalOnly:
  527. return fyne.NewSize(32, min.Height)
  528. case widget.ScrollVerticalOnly:
  529. return fyne.NewSize(min.Width, 32)
  530. default:
  531. return min
  532. }
  533. }
  534. func (r *textRenderer) calculateMin(bounds []rowBoundary, wrap fyne.TextWrap, objs []fyne.CanvasObject, charMinSize fyne.Size) fyne.Size {
  535. height := float32(0)
  536. width := float32(0)
  537. rowHeight := float32(0)
  538. rowWidth := float32(0)
  539. trunc := r.obj.Truncation
  540. // Accessing the theme here is slow, so we cache the value
  541. lineSpacing := theme.LineSpacing()
  542. i := 0
  543. for row, bound := range bounds {
  544. for range bound.segments {
  545. if i == len(objs) {
  546. break // Refresh may not have created all objects for all rows yet...
  547. }
  548. obj := objs[i]
  549. i++
  550. min := obj.MinSize()
  551. if img, ok := obj.(*richImage); ok {
  552. if !img.MinSize().Subtract(img.oldMin).IsZero() {
  553. img.oldMin = img.MinSize()
  554. min := r.calculateMin(bounds, wrap, objs, charMinSize)
  555. if r.obj.scr != nil {
  556. r.obj.prop.SetMinSize(min)
  557. }
  558. r.Refresh() // TODO resolve this in a similar way to #2991
  559. }
  560. }
  561. rowHeight = fyne.Max(rowHeight, min.Height)
  562. rowWidth += min.Width
  563. }
  564. if wrap == fyne.TextWrapOff && trunc == fyne.TextTruncateOff {
  565. width = fyne.Max(width, rowWidth)
  566. }
  567. height += rowHeight
  568. rowHeight = 0
  569. rowWidth = 0
  570. lastSeg := bound.segments[len(bound.segments)-1]
  571. if !lastSeg.Inline() && row < len(bounds)-1 && bounds[row+1].segments[0] != lastSeg { // ignore wrapped lines etc
  572. height += lineSpacing
  573. }
  574. }
  575. if height == 0 {
  576. height = charMinSize.Height
  577. }
  578. return fyne.NewSize(width, height).
  579. Add(fyne.NewSquareSize(theme.InnerPadding() * 2).Subtract(r.obj.inset).Subtract(r.obj.inset))
  580. }
  581. func (r *textRenderer) Refresh() {
  582. r.obj.propertyLock.RLock()
  583. bounds := r.obj.rowBounds
  584. scroll := r.obj.Scroll
  585. r.obj.propertyLock.RUnlock()
  586. var objs []fyne.CanvasObject
  587. for _, bound := range bounds {
  588. for i, seg := range bound.segments {
  589. if _, ok := seg.(*TextSegment); !ok {
  590. obj := r.obj.cachedSegmentVisual(seg, 0)
  591. seg.Update(obj)
  592. objs = append(objs, obj)
  593. continue
  594. }
  595. reuse := 0
  596. if i == 0 {
  597. reuse = bound.firstSegmentReuse
  598. }
  599. obj := r.obj.cachedSegmentVisual(seg, reuse)
  600. seg.Update(obj)
  601. txt := obj.(*canvas.Text)
  602. textSeg := seg.(*TextSegment)
  603. runes := []rune(textSeg.Text)
  604. if i == 0 {
  605. if len(bound.segments) == 1 {
  606. txt.Text = string(runes[bound.begin:bound.end])
  607. } else {
  608. txt.Text = string(runes[bound.begin:])
  609. }
  610. } else if i == len(bound.segments)-1 && len(bound.segments) > 1 {
  611. txt.Text = string(runes[:bound.end])
  612. }
  613. if bound.ellipsis && i == len(bound.segments)-1 {
  614. txt.Text = txt.Text + "…"
  615. }
  616. if concealed(seg) {
  617. txt.Text = strings.Repeat(passwordChar, len(runes))
  618. }
  619. objs = append(objs, txt)
  620. }
  621. }
  622. r.obj.propertyLock.Lock()
  623. if r.obj.scr != nil {
  624. r.obj.scr.Content = &fyne.Container{Layout: layout.NewStackLayout(), Objects: []fyne.CanvasObject{
  625. r.obj.prop, &fyne.Container{Objects: objs}}}
  626. r.obj.scr.Direction = scroll
  627. r.SetObjects([]fyne.CanvasObject{r.obj.scr})
  628. r.obj.scr.Refresh()
  629. } else {
  630. r.SetObjects(objs)
  631. }
  632. r.obj.propertyLock.Unlock()
  633. r.Layout(r.obj.Size())
  634. }
  635. func (r *textRenderer) layoutRow(texts []fyne.CanvasObject, align fyne.TextAlign, xPos, yPos, lineWidth float32) (float32, float32) {
  636. initialX := xPos
  637. if len(texts) == 1 {
  638. min := texts[0].MinSize()
  639. if text, ok := texts[0].(*canvas.Text); ok {
  640. texts[0].Resize(min)
  641. xPad := float32(0)
  642. switch text.Alignment {
  643. case fyne.TextAlignLeading:
  644. case fyne.TextAlignTrailing:
  645. xPad = lineWidth - min.Width
  646. case fyne.TextAlignCenter:
  647. xPad = (lineWidth - min.Width) / 2
  648. }
  649. texts[0].Move(fyne.NewPos(xPos+xPad, yPos))
  650. } else {
  651. texts[0].Resize(fyne.NewSize(lineWidth, min.Height))
  652. texts[0].Move(fyne.NewPos(xPos, yPos))
  653. }
  654. return min.Width, min.Height
  655. }
  656. height := float32(0)
  657. tallestBaseline := float32(0)
  658. realign := false
  659. baselines := make([]float32, len(texts))
  660. // Access to theme is slow, so we cache the text size
  661. textSize := theme.TextSize()
  662. for i, text := range texts {
  663. var size fyne.Size
  664. if txt, ok := text.(*canvas.Text); ok {
  665. s, base := fyne.CurrentApp().Driver().RenderedTextSize(txt.Text, txt.TextSize, txt.TextStyle)
  666. if base > tallestBaseline {
  667. if tallestBaseline > 0 {
  668. realign = true
  669. }
  670. tallestBaseline = base
  671. }
  672. size = s
  673. baselines[i] = base
  674. } else if c, ok := text.(*fyne.Container); ok {
  675. wid := c.Objects[0]
  676. if link, ok := wid.(*Hyperlink); ok {
  677. s, base := fyne.CurrentApp().Driver().RenderedTextSize(link.Text, textSize, link.TextStyle)
  678. if base > tallestBaseline {
  679. if tallestBaseline > 0 {
  680. realign = true
  681. }
  682. tallestBaseline = base
  683. }
  684. size = s
  685. baselines[i] = base
  686. }
  687. }
  688. if size.IsZero() {
  689. size = text.MinSize()
  690. }
  691. text.Resize(size)
  692. text.Move(fyne.NewPos(xPos, yPos))
  693. xPos += size.Width
  694. if height == 0 {
  695. height = size.Height
  696. } else if height != size.Height {
  697. height = fyne.Max(height, size.Height)
  698. realign = true
  699. }
  700. }
  701. if realign {
  702. for i, text := range texts {
  703. delta := tallestBaseline - baselines[i]
  704. text.Move(fyne.NewPos(text.Position().X, yPos+delta))
  705. }
  706. }
  707. spare := lineWidth - xPos
  708. switch align {
  709. case fyne.TextAlignTrailing:
  710. first := texts[0]
  711. first.Resize(fyne.NewSize(first.Size().Width+spare, height))
  712. setAlign(first, fyne.TextAlignTrailing)
  713. for _, text := range texts[1:] {
  714. text.Move(text.Position().Add(fyne.NewPos(spare, 0)))
  715. }
  716. case fyne.TextAlignCenter:
  717. pad := spare / 2
  718. first := texts[0]
  719. first.Resize(fyne.NewSize(first.Size().Width+pad, height))
  720. setAlign(first, fyne.TextAlignTrailing)
  721. last := texts[len(texts)-1]
  722. last.Resize(fyne.NewSize(last.Size().Width+pad, height))
  723. setAlign(last, fyne.TextAlignLeading)
  724. for _, text := range texts[1:] {
  725. text.Move(text.Position().Add(fyne.NewPos(pad, 0)))
  726. }
  727. default:
  728. last := texts[len(texts)-1]
  729. last.Resize(fyne.NewSize(last.Size().Width+spare, height))
  730. setAlign(last, fyne.TextAlignLeading)
  731. }
  732. return xPos - initialX, height
  733. }
  734. // binarySearch accepts a function that checks if the text width less the maximum width and the start and end rune index
  735. // binarySearch returns the index of rune located as close to the maximum line width as possible
  736. func binarySearch(lessMaxWidth func(int, int) bool, low int, maxHigh int) int {
  737. if low >= maxHigh {
  738. return low
  739. }
  740. if lessMaxWidth(low, maxHigh) {
  741. return maxHigh
  742. }
  743. high := low
  744. delta := maxHigh - low
  745. for delta > 0 {
  746. delta /= 2
  747. if lessMaxWidth(low, high+delta) {
  748. high += delta
  749. }
  750. }
  751. for (high < maxHigh) && lessMaxWidth(low, high+1) {
  752. high++
  753. }
  754. return high
  755. }
  756. // concealed returns true if the segment represents a password, meaning the text should be obscured.
  757. func concealed(seg RichTextSegment) bool {
  758. if text, ok := seg.(*TextSegment); ok {
  759. return text.Style.concealed
  760. }
  761. return false
  762. }
  763. func ellipsisPriorBound(bounds []rowBoundary, trunc fyne.TextTruncation, width float32, measurer func([]rune) fyne.Size) []rowBoundary {
  764. if trunc != fyne.TextTruncateEllipsis || len(bounds) == 0 {
  765. return bounds
  766. }
  767. prior := bounds[len(bounds)-1]
  768. seg := prior.segments[0].(*TextSegment)
  769. ellipsisSize := fyne.MeasureText("…", seg.size(), seg.Style.TextStyle)
  770. widthChecker := func(low int, high int) bool {
  771. return measurer([]rune(seg.Text)[low:high]).Width <= width-ellipsisSize.Width
  772. }
  773. limit := binarySearch(widthChecker, prior.begin, prior.end)
  774. prior.end = limit
  775. prior.ellipsis = true
  776. bounds[len(bounds)-1] = prior
  777. return bounds
  778. }
  779. // findSpaceIndex accepts a slice of runes and a fallback index
  780. // findSpaceIndex returns the index of the last space in the text, or fallback if there are no spaces
  781. func findSpaceIndex(text []rune, fallback int) int {
  782. curIndex := fallback
  783. for ; curIndex >= 0; curIndex-- {
  784. if unicode.IsSpace(text[curIndex]) {
  785. break
  786. }
  787. }
  788. if curIndex < 0 {
  789. return fallback
  790. }
  791. return curIndex
  792. }
  793. func float32ToFixed266(f float32) fixed.Int26_6 {
  794. return fixed.Int26_6(float64(f) * (1 << 6))
  795. }
  796. // lineBounds accepts a slice of Segments, a wrapping mode, a maximum size available to display and a function to
  797. // measure text size.
  798. // It will return a slice containing the boundary metadata of each line with the given wrapping applied and the
  799. // total height required to render the boundaries at the given width/height constraints
  800. func lineBounds(seg *TextSegment, wrap fyne.TextWrap, trunc fyne.TextTruncation, firstWidth float32, max fyne.Size, measurer func([]rune) fyne.Size) ([]rowBoundary, float32) {
  801. lines := splitLines(seg)
  802. if wrap == fyne.TextTruncate {
  803. if trunc == fyne.TextTruncateOff {
  804. trunc = fyne.TextTruncateClip
  805. }
  806. wrap = fyne.TextWrapOff
  807. }
  808. if max.Width < 0 || wrap == fyne.TextWrapOff && trunc == fyne.TextTruncateOff {
  809. return lines, 0 // don't bother returning a calculated height, our MinSize is going to cover it
  810. }
  811. measureWidth := float32(math.Min(float64(firstWidth), float64(max.Width)))
  812. text := []rune(seg.Text)
  813. widthChecker := func(low int, high int) bool {
  814. return measurer(text[low:high]).Width <= measureWidth
  815. }
  816. reuse := 0
  817. yPos := float32(0)
  818. var bounds []rowBoundary
  819. for _, l := range lines {
  820. low := l.begin
  821. high := l.end
  822. if low == high {
  823. l.firstSegmentReuse = reuse
  824. reuse++
  825. bounds = append(bounds, l)
  826. continue
  827. }
  828. switch wrap {
  829. case fyne.TextWrapBreak:
  830. for low < high {
  831. measured := measurer(text[low:high])
  832. if yPos+measured.Height > max.Height && trunc != fyne.TextTruncateOff {
  833. return ellipsisPriorBound(bounds, trunc, measureWidth, measurer), yPos
  834. }
  835. if measured.Width <= measureWidth {
  836. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, false})
  837. reuse++
  838. low = high
  839. high = l.end
  840. measureWidth = max.Width
  841. yPos += measured.Height
  842. } else {
  843. newHigh := binarySearch(widthChecker, low, high)
  844. if newHigh <= low {
  845. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, low + 1, false})
  846. reuse++
  847. low++
  848. yPos += measured.Height
  849. } else {
  850. high = newHigh
  851. }
  852. }
  853. }
  854. case fyne.TextWrapWord:
  855. for low < high {
  856. sub := text[low:high]
  857. measured := measurer(sub)
  858. if yPos+measured.Height > max.Height && trunc != fyne.TextTruncateOff {
  859. return ellipsisPriorBound(bounds, trunc, measureWidth, measurer), yPos
  860. }
  861. subWidth := measured.Width
  862. if subWidth <= measureWidth {
  863. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, false})
  864. reuse++
  865. low = high
  866. high = l.end
  867. if low < high && unicode.IsSpace(text[low]) {
  868. low++
  869. }
  870. measureWidth = max.Width
  871. yPos += measured.Height
  872. } else {
  873. oldHigh := high
  874. last := low + len(sub) - 1
  875. fallback := binarySearch(widthChecker, low, last) - low
  876. if fallback < 1 { // even a character won't fit
  877. include := 1
  878. ellipsis := false
  879. if trunc == fyne.TextTruncateEllipsis {
  880. include = 0
  881. ellipsis = true
  882. }
  883. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, low + include, ellipsis})
  884. low++
  885. high = low + 1
  886. reuse++
  887. yPos += measured.Height
  888. if high > l.end {
  889. return bounds, yPos
  890. }
  891. } else {
  892. spaceIndex := findSpaceIndex(sub, fallback)
  893. if spaceIndex == 0 {
  894. spaceIndex = 1
  895. }
  896. high = low + spaceIndex
  897. }
  898. if high == fallback && subWidth <= max.Width { // add a newline as there is more space on next
  899. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, low, false})
  900. reuse++
  901. high = oldHigh
  902. measureWidth = max.Width
  903. yPos += measured.Height
  904. continue
  905. }
  906. }
  907. }
  908. default:
  909. if trunc == fyne.TextTruncateEllipsis {
  910. txt := seg.Text[low:high]
  911. end, full := truncateLimit(txt, seg.Visual().(*canvas.Text), int(measureWidth), []rune{'…'})
  912. high = low + end
  913. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, !full})
  914. reuse++
  915. } else if trunc == fyne.TextTruncateClip {
  916. high = binarySearch(widthChecker, low, high)
  917. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, false})
  918. reuse++
  919. }
  920. }
  921. }
  922. return bounds, yPos
  923. }
  924. func setAlign(obj fyne.CanvasObject, align fyne.TextAlign) {
  925. if text, ok := obj.(*canvas.Text); ok {
  926. text.Alignment = align
  927. return
  928. }
  929. if c, ok := obj.(*fyne.Container); ok {
  930. wid := c.Objects[0]
  931. if link, ok := wid.(*Hyperlink); ok {
  932. link.Alignment = align
  933. link.Refresh()
  934. }
  935. }
  936. }
  937. // splitLines accepts a text segment and returns a slice of boundary metadata denoting the
  938. // start and end indices of each line delimited by the newline character.
  939. func splitLines(seg *TextSegment) []rowBoundary {
  940. var low, high int
  941. var lines []rowBoundary
  942. text := []rune(seg.Text)
  943. length := len(text)
  944. for i := 0; i < length; i++ {
  945. if text[i] == '\n' {
  946. high = i
  947. lines = append(lines, rowBoundary{[]RichTextSegment{seg}, len(lines), low, high, false})
  948. low = i + 1
  949. }
  950. }
  951. return append(lines, rowBoundary{[]RichTextSegment{seg}, len(lines), low, length, false})
  952. }
  953. func truncateLimit(s string, text *canvas.Text, limit int, ellipsis []rune) (int, bool) {
  954. face := paint.CachedFontFace(text.TextStyle, text.TextSize, 1.0)
  955. runes := []rune(s)
  956. in := shaping.Input{
  957. Text: ellipsis,
  958. RunStart: 0,
  959. RunEnd: len(ellipsis),
  960. Direction: di.DirectionLTR,
  961. Face: face.Fonts[0],
  962. Size: float32ToFixed266(text.TextSize),
  963. }
  964. shaper := &shaping.HarfbuzzShaper{}
  965. conf := shaping.WrapConfig{}
  966. conf = conf.WithTruncator(shaper, in)
  967. conf.BreakPolicy = shaping.WhenNecessary
  968. conf.TruncateAfterLines = 1
  969. l := shaping.LineWrapper{}
  970. in.Text = runes
  971. in.RunEnd = len(runes)
  972. out := shaper.Shape(in)
  973. l.Prepare(conf, runes, shaping.NewSliceIterator([]shaping.Output{out}))
  974. finalLine, _, done := l.WrapNextLine(limit)
  975. count := finalLine[0].Runes.Count
  976. full := done && count == len(runes)
  977. if !full && len(ellipsis) > 0 {
  978. count--
  979. }
  980. return count, full
  981. }
  982. type rowBoundary struct {
  983. segments []RichTextSegment
  984. firstSegmentReuse int
  985. begin, end int
  986. ellipsis bool
  987. }