richtext.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107
  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. canvas.Refresh(r.obj.super())
  635. }
  636. func (r *textRenderer) layoutRow(texts []fyne.CanvasObject, align fyne.TextAlign, xPos, yPos, lineWidth float32) (float32, float32) {
  637. initialX := xPos
  638. if len(texts) == 1 {
  639. min := texts[0].MinSize()
  640. if text, ok := texts[0].(*canvas.Text); ok {
  641. texts[0].Resize(min)
  642. xPad := float32(0)
  643. switch text.Alignment {
  644. case fyne.TextAlignLeading:
  645. case fyne.TextAlignTrailing:
  646. xPad = lineWidth - min.Width
  647. case fyne.TextAlignCenter:
  648. xPad = (lineWidth - min.Width) / 2
  649. }
  650. texts[0].Move(fyne.NewPos(xPos+xPad, yPos))
  651. } else {
  652. texts[0].Resize(fyne.NewSize(lineWidth, min.Height))
  653. texts[0].Move(fyne.NewPos(xPos, yPos))
  654. }
  655. return min.Width, min.Height
  656. }
  657. height := float32(0)
  658. tallestBaseline := float32(0)
  659. realign := false
  660. baselines := make([]float32, len(texts))
  661. // Access to theme is slow, so we cache the text size
  662. textSize := theme.TextSize()
  663. for i, text := range texts {
  664. var size fyne.Size
  665. if txt, ok := text.(*canvas.Text); ok {
  666. s, base := fyne.CurrentApp().Driver().RenderedTextSize(txt.Text, txt.TextSize, txt.TextStyle)
  667. if base > tallestBaseline {
  668. if tallestBaseline > 0 {
  669. realign = true
  670. }
  671. tallestBaseline = base
  672. }
  673. size = s
  674. baselines[i] = base
  675. } else if c, ok := text.(*fyne.Container); ok {
  676. wid := c.Objects[0]
  677. if link, ok := wid.(*Hyperlink); ok {
  678. s, base := fyne.CurrentApp().Driver().RenderedTextSize(link.Text, textSize, link.TextStyle)
  679. if base > tallestBaseline {
  680. if tallestBaseline > 0 {
  681. realign = true
  682. }
  683. tallestBaseline = base
  684. }
  685. size = s
  686. baselines[i] = base
  687. }
  688. }
  689. if size.IsZero() {
  690. size = text.MinSize()
  691. }
  692. text.Resize(size)
  693. text.Move(fyne.NewPos(xPos, yPos))
  694. xPos += size.Width
  695. if height == 0 {
  696. height = size.Height
  697. } else if height != size.Height {
  698. height = fyne.Max(height, size.Height)
  699. realign = true
  700. }
  701. }
  702. if realign {
  703. for i, text := range texts {
  704. delta := tallestBaseline - baselines[i]
  705. text.Move(fyne.NewPos(text.Position().X, yPos+delta))
  706. }
  707. }
  708. spare := lineWidth - xPos
  709. switch align {
  710. case fyne.TextAlignTrailing:
  711. first := texts[0]
  712. first.Resize(fyne.NewSize(first.Size().Width+spare, height))
  713. setAlign(first, fyne.TextAlignTrailing)
  714. for _, text := range texts[1:] {
  715. text.Move(text.Position().Add(fyne.NewPos(spare, 0)))
  716. }
  717. case fyne.TextAlignCenter:
  718. pad := spare / 2
  719. first := texts[0]
  720. first.Resize(fyne.NewSize(first.Size().Width+pad, height))
  721. setAlign(first, fyne.TextAlignTrailing)
  722. last := texts[len(texts)-1]
  723. last.Resize(fyne.NewSize(last.Size().Width+pad, height))
  724. setAlign(last, fyne.TextAlignLeading)
  725. for _, text := range texts[1:] {
  726. text.Move(text.Position().Add(fyne.NewPos(pad, 0)))
  727. }
  728. default:
  729. last := texts[len(texts)-1]
  730. last.Resize(fyne.NewSize(last.Size().Width+spare, height))
  731. setAlign(last, fyne.TextAlignLeading)
  732. }
  733. return xPos - initialX, height
  734. }
  735. // binarySearch accepts a function that checks if the text width less the maximum width and the start and end rune index
  736. // binarySearch returns the index of rune located as close to the maximum line width as possible
  737. func binarySearch(lessMaxWidth func(int, int) bool, low int, maxHigh int) int {
  738. if low >= maxHigh {
  739. return low
  740. }
  741. if lessMaxWidth(low, maxHigh) {
  742. return maxHigh
  743. }
  744. high := low
  745. delta := maxHigh - low
  746. for delta > 0 {
  747. delta /= 2
  748. if lessMaxWidth(low, high+delta) {
  749. high += delta
  750. }
  751. }
  752. for (high < maxHigh) && lessMaxWidth(low, high+1) {
  753. high++
  754. }
  755. return high
  756. }
  757. // concealed returns true if the segment represents a password, meaning the text should be obscured.
  758. func concealed(seg RichTextSegment) bool {
  759. if text, ok := seg.(*TextSegment); ok {
  760. return text.Style.concealed
  761. }
  762. return false
  763. }
  764. func ellipsisPriorBound(bounds []rowBoundary, trunc fyne.TextTruncation, width float32, measurer func([]rune) fyne.Size) []rowBoundary {
  765. if trunc != fyne.TextTruncateEllipsis || len(bounds) == 0 {
  766. return bounds
  767. }
  768. prior := bounds[len(bounds)-1]
  769. seg := prior.segments[0].(*TextSegment)
  770. ellipsisSize := fyne.MeasureText("…", seg.size(), seg.Style.TextStyle)
  771. widthChecker := func(low int, high int) bool {
  772. return measurer([]rune(seg.Text)[low:high]).Width <= width-ellipsisSize.Width
  773. }
  774. limit := binarySearch(widthChecker, prior.begin, prior.end)
  775. prior.end = limit
  776. prior.ellipsis = true
  777. bounds[len(bounds)-1] = prior
  778. return bounds
  779. }
  780. // findSpaceIndex accepts a slice of runes and a fallback index
  781. // findSpaceIndex returns the index of the last space in the text, or fallback if there are no spaces
  782. func findSpaceIndex(text []rune, fallback int) int {
  783. curIndex := fallback
  784. for ; curIndex >= 0; curIndex-- {
  785. if unicode.IsSpace(text[curIndex]) {
  786. break
  787. }
  788. }
  789. if curIndex < 0 {
  790. return fallback
  791. }
  792. return curIndex
  793. }
  794. func float32ToFixed266(f float32) fixed.Int26_6 {
  795. return fixed.Int26_6(float64(f) * (1 << 6))
  796. }
  797. // lineBounds accepts a slice of Segments, a wrapping mode, a maximum size available to display and a function to
  798. // measure text size.
  799. // It will return a slice containing the boundary metadata of each line with the given wrapping applied and the
  800. // total height required to render the boundaries at the given width/height constraints
  801. func lineBounds(seg *TextSegment, wrap fyne.TextWrap, trunc fyne.TextTruncation, firstWidth float32, max fyne.Size, measurer func([]rune) fyne.Size) ([]rowBoundary, float32) {
  802. lines := splitLines(seg)
  803. if wrap == fyne.TextTruncate {
  804. if trunc == fyne.TextTruncateOff {
  805. trunc = fyne.TextTruncateClip
  806. }
  807. wrap = fyne.TextWrapOff
  808. }
  809. if max.Width < 0 || wrap == fyne.TextWrapOff && trunc == fyne.TextTruncateOff {
  810. return lines, 0 // don't bother returning a calculated height, our MinSize is going to cover it
  811. }
  812. measureWidth := float32(math.Min(float64(firstWidth), float64(max.Width)))
  813. text := []rune(seg.Text)
  814. widthChecker := func(low int, high int) bool {
  815. return measurer(text[low:high]).Width <= measureWidth
  816. }
  817. reuse := 0
  818. yPos := float32(0)
  819. var bounds []rowBoundary
  820. for _, l := range lines {
  821. low := l.begin
  822. high := l.end
  823. if low == high {
  824. l.firstSegmentReuse = reuse
  825. reuse++
  826. bounds = append(bounds, l)
  827. continue
  828. }
  829. switch wrap {
  830. case fyne.TextWrapBreak:
  831. for low < high {
  832. measured := measurer(text[low:high])
  833. if yPos+measured.Height > max.Height && trunc != fyne.TextTruncateOff {
  834. return ellipsisPriorBound(bounds, trunc, measureWidth, measurer), yPos
  835. }
  836. if measured.Width <= measureWidth {
  837. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, false})
  838. reuse++
  839. low = high
  840. high = l.end
  841. measureWidth = max.Width
  842. yPos += measured.Height
  843. } else {
  844. newHigh := binarySearch(widthChecker, low, high)
  845. if newHigh <= low {
  846. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, low + 1, false})
  847. reuse++
  848. low++
  849. yPos += measured.Height
  850. } else {
  851. high = newHigh
  852. }
  853. }
  854. }
  855. case fyne.TextWrapWord:
  856. for low < high {
  857. sub := text[low:high]
  858. measured := measurer(sub)
  859. if yPos+measured.Height > max.Height && trunc != fyne.TextTruncateOff {
  860. return ellipsisPriorBound(bounds, trunc, measureWidth, measurer), yPos
  861. }
  862. subWidth := measured.Width
  863. if subWidth <= measureWidth {
  864. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, false})
  865. reuse++
  866. low = high
  867. high = l.end
  868. if low < high && unicode.IsSpace(text[low]) {
  869. low++
  870. }
  871. measureWidth = max.Width
  872. yPos += measured.Height
  873. } else {
  874. oldHigh := high
  875. last := low + len(sub) - 1
  876. fallback := binarySearch(widthChecker, low, last) - low
  877. if fallback < 1 { // even a character won't fit
  878. include := 1
  879. ellipsis := false
  880. if trunc == fyne.TextTruncateEllipsis {
  881. include = 0
  882. ellipsis = true
  883. }
  884. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, low + include, ellipsis})
  885. low++
  886. high = low + 1
  887. reuse++
  888. yPos += measured.Height
  889. if high > l.end {
  890. return bounds, yPos
  891. }
  892. } else {
  893. spaceIndex := findSpaceIndex(sub, fallback)
  894. if spaceIndex == 0 {
  895. spaceIndex = 1
  896. }
  897. high = low + spaceIndex
  898. }
  899. if high == fallback && subWidth <= max.Width { // add a newline as there is more space on next
  900. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, low, false})
  901. reuse++
  902. high = oldHigh
  903. measureWidth = max.Width
  904. yPos += measured.Height
  905. continue
  906. }
  907. }
  908. }
  909. default:
  910. if trunc == fyne.TextTruncateEllipsis {
  911. txt := []rune(seg.Text)[low:high]
  912. end, full := truncateLimit(string(txt), seg.Visual().(*canvas.Text), int(measureWidth), []rune{'…'})
  913. high = low + end
  914. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, !full})
  915. reuse++
  916. } else if trunc == fyne.TextTruncateClip {
  917. high = binarySearch(widthChecker, low, high)
  918. bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, false})
  919. reuse++
  920. }
  921. }
  922. }
  923. return bounds, yPos
  924. }
  925. func setAlign(obj fyne.CanvasObject, align fyne.TextAlign) {
  926. if text, ok := obj.(*canvas.Text); ok {
  927. text.Alignment = align
  928. return
  929. }
  930. if c, ok := obj.(*fyne.Container); ok {
  931. wid := c.Objects[0]
  932. if link, ok := wid.(*Hyperlink); ok {
  933. link.Alignment = align
  934. link.Refresh()
  935. }
  936. }
  937. }
  938. // splitLines accepts a text segment and returns a slice of boundary metadata denoting the
  939. // start and end indices of each line delimited by the newline character.
  940. func splitLines(seg *TextSegment) []rowBoundary {
  941. var low, high int
  942. var lines []rowBoundary
  943. text := []rune(seg.Text)
  944. length := len(text)
  945. for i := 0; i < length; i++ {
  946. if text[i] == '\n' {
  947. high = i
  948. lines = append(lines, rowBoundary{[]RichTextSegment{seg}, len(lines), low, high, false})
  949. low = i + 1
  950. }
  951. }
  952. return append(lines, rowBoundary{[]RichTextSegment{seg}, len(lines), low, length, false})
  953. }
  954. func truncateLimit(s string, text *canvas.Text, limit int, ellipsis []rune) (int, bool) {
  955. face := paint.CachedFontFace(text.TextStyle, text.TextSize, 1.0)
  956. runes := []rune(s)
  957. in := shaping.Input{
  958. Text: ellipsis,
  959. RunStart: 0,
  960. RunEnd: len(ellipsis),
  961. Direction: di.DirectionLTR,
  962. Face: face.Fonts[0],
  963. Size: float32ToFixed266(text.TextSize),
  964. }
  965. shaper := &shaping.HarfbuzzShaper{}
  966. conf := shaping.WrapConfig{}
  967. conf = conf.WithTruncator(shaper, in)
  968. conf.BreakPolicy = shaping.WhenNecessary
  969. conf.TruncateAfterLines = 1
  970. l := shaping.LineWrapper{}
  971. in.Text = runes
  972. in.RunEnd = len(runes)
  973. out := shaper.Shape(in)
  974. l.Prepare(conf, runes, shaping.NewSliceIterator([]shaping.Output{out}))
  975. finalLine, _, done := l.WrapNextLine(limit)
  976. count := finalLine[0].Runes.Count
  977. full := done && count == len(runes)
  978. if !full && len(ellipsis) > 0 {
  979. count--
  980. }
  981. return count, full
  982. }
  983. type rowBoundary struct {
  984. segments []RichTextSegment
  985. firstSegmentReuse int
  986. begin, end int
  987. ellipsis bool
  988. }