markup_renderer.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. package test
  2. import (
  3. "fmt"
  4. "image/color"
  5. "reflect"
  6. "sort"
  7. "strings"
  8. "unsafe"
  9. "fyne.io/fyne/v2"
  10. "fyne.io/fyne/v2/canvas"
  11. col "fyne.io/fyne/v2/internal/color"
  12. "fyne.io/fyne/v2/internal/driver"
  13. "fyne.io/fyne/v2/layout"
  14. "fyne.io/fyne/v2/theme"
  15. )
  16. type markupRenderer struct {
  17. indentation int
  18. w strings.Builder
  19. }
  20. // snapshot creates a new snapshot of the current render tree.
  21. func snapshot(c fyne.Canvas) string {
  22. r := markupRenderer{}
  23. r.writeCanvas(c)
  24. return r.w.String()
  25. }
  26. func (r *markupRenderer) setAlignmentAttr(attrs map[string]*string, name string, a fyne.TextAlign) {
  27. var value string
  28. switch a {
  29. case fyne.TextAlignLeading:
  30. // default mode, don’t add an attr
  31. case fyne.TextAlignCenter:
  32. value = "center"
  33. case fyne.TextAlignTrailing:
  34. value = "trailing"
  35. default:
  36. value = fmt.Sprintf("unknown alignment: %d", a)
  37. }
  38. r.setStringAttr(attrs, name, value)
  39. }
  40. func (r *markupRenderer) setBoolAttr(attrs map[string]*string, name string, b bool) {
  41. if !b {
  42. return
  43. }
  44. attrs[name] = nil
  45. }
  46. func (r *markupRenderer) setColorAttr(attrs map[string]*string, name string, c color.Color) {
  47. r.setColorAttrWithDefault(attrs, name, c, color.Transparent)
  48. }
  49. func (r *markupRenderer) setColorAttrWithDefault(attrs map[string]*string, name string, c color.Color, d color.Color) {
  50. if c == nil || c == d {
  51. return
  52. }
  53. if value := knownColor(c); value != "" {
  54. r.setStringAttr(attrs, name, value)
  55. return
  56. }
  57. for _, n := range theme.PrimaryColorNames() {
  58. if c == theme.PrimaryColorNamed(n) {
  59. r.setStringAttr(attrs, name, n)
  60. return
  61. }
  62. }
  63. rd, g, b, a := col.ToNRGBA(c)
  64. r.setStringAttr(attrs, name, fmt.Sprintf("rgba(%d,%d,%d,%d)", uint8(rd), uint8(g), uint8(b), uint8(a)))
  65. }
  66. func (r *markupRenderer) setFillModeAttr(attrs map[string]*string, name string, m canvas.ImageFill) {
  67. var fillMode string
  68. switch m {
  69. case canvas.ImageFillStretch:
  70. // default mode, don’t add an attr
  71. case canvas.ImageFillContain:
  72. fillMode = "contain"
  73. case canvas.ImageFillOriginal:
  74. fillMode = "original"
  75. default:
  76. fillMode = fmt.Sprintf("unknown fill mode: %d", m)
  77. }
  78. r.setStringAttr(attrs, name, fillMode)
  79. }
  80. func (r *markupRenderer) setFloatAttr(attrs map[string]*string, name string, f float64) {
  81. r.setFloatAttrWithDefault(attrs, name, f, 0)
  82. }
  83. func (r *markupRenderer) setFloatAttrWithDefault(attrs map[string]*string, name string, f float64, d float64) {
  84. if f == d {
  85. return
  86. }
  87. value := fmt.Sprintf("%g", f)
  88. attrs[name] = &value
  89. }
  90. func (r *markupRenderer) setFloatPosAttr(attrs map[string]*string, name string, x, y float64) {
  91. if x == 0 && y == 0 {
  92. return
  93. }
  94. value := fmt.Sprintf("%g,%g", x, y)
  95. attrs[name] = &value
  96. }
  97. func (r *markupRenderer) setSizeAttrWithDefault(attrs map[string]*string, name string, i float32, d float32) {
  98. if int(i) == int(d) {
  99. return
  100. }
  101. value := fmt.Sprintf("%d", int(i))
  102. attrs[name] = &value
  103. }
  104. func (r *markupRenderer) setPosAttr(attrs map[string]*string, name string, pos fyne.Position) {
  105. if int(pos.X) == 0 && int(pos.Y) == 0 {
  106. return
  107. }
  108. value := fmt.Sprintf("%d,%d", int(pos.X), int(pos.Y))
  109. attrs[name] = &value
  110. }
  111. func (r *markupRenderer) setResourceAttr(attrs map[string]*string, name string, rsc fyne.Resource) {
  112. if rsc == nil {
  113. return
  114. }
  115. if value := knownResource(rsc); value != "" {
  116. r.setStringAttr(attrs, name, value)
  117. return
  118. }
  119. var variant string
  120. switch t := rsc.(type) {
  121. case *theme.DisabledResource:
  122. variant = "disabled"
  123. case *theme.ErrorThemedResource:
  124. variant = "error"
  125. case *theme.InvertedThemedResource:
  126. variant = "inverted"
  127. case *theme.PrimaryThemedResource:
  128. variant = "primary"
  129. case *theme.ThemedResource:
  130. variant = string(t.ColorName)
  131. if variant == "" {
  132. variant = "default"
  133. }
  134. default:
  135. r.setStringAttr(attrs, name, rsc.Name())
  136. return
  137. }
  138. // That’s some magic to access the private `source` field of the themed resource.
  139. v := reflect.ValueOf(rsc).Elem().Field(0)
  140. src := reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem().Interface().(fyne.Resource)
  141. r.setResourceAttr(attrs, name, src)
  142. r.setStringAttr(attrs, "themed", variant)
  143. }
  144. func (r *markupRenderer) setScaleModeAttr(attrs map[string]*string, name string, m canvas.ImageScale) {
  145. var scaleMode string
  146. switch m {
  147. case canvas.ImageScaleSmooth:
  148. // default mode, don’t add an attr
  149. case canvas.ImageScalePixels:
  150. scaleMode = "pixels"
  151. default:
  152. scaleMode = fmt.Sprintf("unknown scale mode: %d", m)
  153. }
  154. r.setStringAttr(attrs, name, scaleMode)
  155. }
  156. func (r *markupRenderer) setSizeAttr(attrs map[string]*string, name string, size fyne.Size) {
  157. value := fmt.Sprintf("%dx%d", int(size.Width), int(size.Height))
  158. attrs[name] = &value
  159. }
  160. func (r *markupRenderer) setStringAttr(attrs map[string]*string, name string, s string) {
  161. if s == "" {
  162. return
  163. }
  164. attrs[name] = &s
  165. }
  166. func (r *markupRenderer) writeCanvas(c fyne.Canvas) {
  167. attrs := map[string]*string{}
  168. r.setSizeAttr(attrs, "size", c.Size())
  169. if tc, ok := c.(WindowlessCanvas); ok {
  170. r.setBoolAttr(attrs, "padded", tc.Padded())
  171. }
  172. r.writeTag("canvas", false, attrs)
  173. r.w.WriteRune('\n')
  174. r.indentation++
  175. r.writeTag("content", false, nil)
  176. r.w.WriteRune('\n')
  177. r.indentation++
  178. driver.WalkVisibleObjectTree(c.Content(), r.writeCanvasObject, r.writeCloseCanvasObject)
  179. r.indentation--
  180. r.writeIndent()
  181. r.writeCloseTag("content")
  182. for _, o := range c.Overlays().List() {
  183. r.writeTag("overlay", false, nil)
  184. r.w.WriteRune('\n')
  185. r.indentation++
  186. driver.WalkVisibleObjectTree(o, r.writeCanvasObject, r.writeCloseCanvasObject)
  187. r.indentation--
  188. r.writeIndent()
  189. r.writeCloseTag("overlay")
  190. }
  191. r.indentation--
  192. r.writeIndent()
  193. r.writeCloseTag("canvas")
  194. }
  195. func (r *markupRenderer) writeCanvasObject(obj fyne.CanvasObject, _, _ fyne.Position, _ fyne.Size) bool {
  196. attrs := map[string]*string{}
  197. r.setPosAttr(attrs, "pos", obj.Position())
  198. r.setSizeAttr(attrs, "size", obj.Size())
  199. switch o := obj.(type) {
  200. case *canvas.Circle:
  201. r.writeCircle(o, attrs)
  202. case *canvas.Image:
  203. r.writeImage(o, attrs)
  204. case *canvas.Line:
  205. r.writeLine(o, attrs)
  206. case *canvas.LinearGradient:
  207. r.writeLinearGradient(o, attrs)
  208. case *canvas.RadialGradient:
  209. r.writeRadialGradient(o, attrs)
  210. case *canvas.Raster:
  211. r.writeRaster(o, attrs)
  212. case *canvas.Rectangle:
  213. r.writeRectangle(o, attrs)
  214. case *canvas.Text:
  215. r.writeText(o, attrs)
  216. case *fyne.Container:
  217. r.writeContainer(o, attrs)
  218. case fyne.Widget:
  219. r.writeWidget(o, attrs)
  220. case *layout.Spacer:
  221. r.writeSpacer(o, attrs)
  222. default:
  223. panic(fmt.Sprint("please add support for", reflect.TypeOf(o)))
  224. }
  225. return false
  226. }
  227. func (r *markupRenderer) writeCircle(c *canvas.Circle, attrs map[string]*string) {
  228. r.setColorAttr(attrs, "fillColor", c.FillColor)
  229. r.setColorAttr(attrs, "strokeColor", c.StrokeColor)
  230. r.setFloatAttr(attrs, "strokeWidth", float64(c.StrokeWidth))
  231. r.writeTag("circle", true, attrs)
  232. }
  233. func (r *markupRenderer) writeCloseCanvasObject(o fyne.CanvasObject, _ fyne.Position, _ fyne.CanvasObject) {
  234. switch o.(type) {
  235. case *fyne.Container:
  236. r.indentation--
  237. r.writeIndent()
  238. r.writeCloseTag("container")
  239. case fyne.Widget:
  240. r.indentation--
  241. r.writeIndent()
  242. r.writeCloseTag("widget")
  243. }
  244. }
  245. func (r *markupRenderer) writeCloseTag(name string) {
  246. r.w.WriteString("</")
  247. r.w.WriteString(name)
  248. r.w.WriteString(">\n")
  249. }
  250. func (r *markupRenderer) writeContainer(c *fyne.Container, attrs map[string]*string) {
  251. r.writeTag("container", false, attrs)
  252. r.w.WriteRune('\n')
  253. r.indentation++
  254. }
  255. func (r *markupRenderer) writeIndent() {
  256. for i := 0; i < r.indentation; i++ {
  257. r.w.WriteRune('\t')
  258. }
  259. }
  260. func (r *markupRenderer) writeImage(i *canvas.Image, attrs map[string]*string) {
  261. r.setStringAttr(attrs, "file", i.File)
  262. r.setResourceAttr(attrs, "rsc", i.Resource)
  263. if i.File == "" && i.Resource == nil {
  264. r.setBoolAttr(attrs, "img", i.Image != nil)
  265. }
  266. r.setFloatAttr(attrs, "translucency", i.Translucency)
  267. r.setFillModeAttr(attrs, "fillMode", i.FillMode)
  268. r.setScaleModeAttr(attrs, "scaleMode", i.ScaleMode)
  269. if i.Size().Width == theme.IconInlineSize() && i.Size().Height == i.Size().Width {
  270. r.setStringAttr(attrs, "size", "iconInlineSize")
  271. }
  272. r.writeTag("image", true, attrs)
  273. }
  274. func (r *markupRenderer) writeLine(l *canvas.Line, attrs map[string]*string) {
  275. r.setColorAttr(attrs, "strokeColor", l.StrokeColor)
  276. r.setFloatAttrWithDefault(attrs, "strokeWidth", float64(l.StrokeWidth), 1)
  277. r.writeTag("line", true, attrs)
  278. }
  279. func (r *markupRenderer) writeLinearGradient(g *canvas.LinearGradient, attrs map[string]*string) {
  280. r.setColorAttr(attrs, "startColor", g.StartColor)
  281. r.setColorAttr(attrs, "endColor", g.EndColor)
  282. r.setFloatAttr(attrs, "angle", g.Angle)
  283. r.writeTag("linearGradient", true, attrs)
  284. }
  285. func (r *markupRenderer) writeRadialGradient(g *canvas.RadialGradient, attrs map[string]*string) {
  286. r.setColorAttr(attrs, "startColor", g.StartColor)
  287. r.setColorAttr(attrs, "endColor", g.EndColor)
  288. r.setFloatPosAttr(attrs, "centerOffset", g.CenterOffsetX, g.CenterOffsetY)
  289. r.writeTag("radialGradient", true, attrs)
  290. }
  291. func (r *markupRenderer) writeRaster(rst *canvas.Raster, attrs map[string]*string) {
  292. r.setFloatAttr(attrs, "translucency", rst.Translucency)
  293. r.writeTag("raster", true, attrs)
  294. }
  295. func (r *markupRenderer) writeRectangle(rct *canvas.Rectangle, attrs map[string]*string) {
  296. r.setColorAttr(attrs, "fillColor", rct.FillColor)
  297. r.setColorAttr(attrs, "strokeColor", rct.StrokeColor)
  298. r.setFloatAttr(attrs, "strokeWidth", float64(rct.StrokeWidth))
  299. r.setFloatAttr(attrs, "radius", float64(rct.CornerRadius))
  300. r.writeTag("rectangle", true, attrs)
  301. }
  302. func (r *markupRenderer) writeSpacer(_ *layout.Spacer, attrs map[string]*string) {
  303. r.writeTag("spacer", true, attrs)
  304. }
  305. func (r *markupRenderer) writeTag(name string, isEmpty bool, attrs map[string]*string) {
  306. r.writeIndent()
  307. r.w.WriteRune('<')
  308. r.w.WriteString(name)
  309. for _, key := range sortedKeys(attrs) {
  310. r.w.WriteRune(' ')
  311. r.w.WriteString(key)
  312. if attrs[key] != nil {
  313. r.w.WriteString("=\"")
  314. r.w.WriteString(*attrs[key])
  315. r.w.WriteRune('"')
  316. }
  317. }
  318. if isEmpty {
  319. r.w.WriteString("/>\n")
  320. } else {
  321. r.w.WriteRune('>')
  322. }
  323. }
  324. func (r *markupRenderer) writeText(t *canvas.Text, attrs map[string]*string) {
  325. r.setColorAttrWithDefault(attrs, "color", t.Color, theme.ForegroundColor())
  326. r.setAlignmentAttr(attrs, "alignment", t.Alignment)
  327. r.setSizeAttrWithDefault(attrs, "textSize", t.TextSize, theme.TextSize())
  328. r.setBoolAttr(attrs, "bold", t.TextStyle.Bold)
  329. r.setBoolAttr(attrs, "italic", t.TextStyle.Italic)
  330. r.setBoolAttr(attrs, "monospace", t.TextStyle.Monospace)
  331. r.writeTag("text", false, attrs)
  332. r.w.WriteString(t.Text)
  333. r.writeCloseTag("text")
  334. }
  335. func (r *markupRenderer) writeWidget(w fyne.Widget, attrs map[string]*string) {
  336. r.setStringAttr(attrs, "type", reflect.TypeOf(w).String())
  337. r.writeTag("widget", false, attrs)
  338. r.w.WriteRune('\n')
  339. r.indentation++
  340. }
  341. func nrgbaColor(c color.Color) color.NRGBA {
  342. // using ColorToNRGBA to avoid problems with colors with 16-bit components or alpha values that aren't 0 or the maximum possible alpha value
  343. r, g, b, a := col.ToNRGBA(c)
  344. return color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}
  345. }
  346. func knownColor(c color.Color) string {
  347. return map[color.Color]string{
  348. nrgbaColor(theme.BackgroundColor()): "background",
  349. nrgbaColor(theme.ButtonColor()): "button",
  350. nrgbaColor(theme.DisabledButtonColor()): "disabled button",
  351. nrgbaColor(theme.DisabledColor()): "disabled",
  352. nrgbaColor(theme.ErrorColor()): "error",
  353. nrgbaColor(theme.FocusColor()): "focus",
  354. nrgbaColor(theme.ForegroundColor()): "foreground",
  355. nrgbaColor(theme.HoverColor()): "hover",
  356. nrgbaColor(theme.InputBackgroundColor()): "inputBackground",
  357. nrgbaColor(theme.InputBorderColor()): "inputBorder",
  358. nrgbaColor(theme.MenuBackgroundColor()): "menuBackground",
  359. nrgbaColor(theme.OverlayBackgroundColor()): "overlayBackground",
  360. nrgbaColor(theme.PlaceHolderColor()): "placeholder",
  361. nrgbaColor(theme.PrimaryColor()): "primary",
  362. nrgbaColor(theme.ScrollBarColor()): "scrollbar",
  363. nrgbaColor(theme.SelectionColor()): "selection",
  364. nrgbaColor(theme.ShadowColor()): "shadow",
  365. }[nrgbaColor(c)]
  366. }
  367. func knownResource(rsc fyne.Resource) string {
  368. return map[fyne.Resource]string{
  369. theme.CancelIcon(): "cancelIcon",
  370. theme.CheckButtonCheckedIcon(): "checkButtonCheckedIcon",
  371. theme.CheckButtonIcon(): "checkButtonIcon",
  372. theme.ColorAchromaticIcon(): "colorAchromaticIcon",
  373. theme.ColorChromaticIcon(): "colorChromaticIcon",
  374. theme.ColorPaletteIcon(): "colorPaletteIcon",
  375. theme.ComputerIcon(): "computerIcon",
  376. theme.ConfirmIcon(): "confirmIcon",
  377. theme.ContentAddIcon(): "contentAddIcon",
  378. theme.ContentClearIcon(): "contentClearIcon",
  379. theme.ContentCopyIcon(): "contentCopyIcon",
  380. theme.ContentCutIcon(): "contentCutIcon",
  381. theme.ContentPasteIcon(): "contentPasteIcon",
  382. theme.ContentRedoIcon(): "contentRedoIcon",
  383. theme.ContentRemoveIcon(): "contentRemoveIcon",
  384. theme.ContentUndoIcon(): "contentUndoIcon",
  385. theme.DeleteIcon(): "deleteIcon",
  386. theme.DocumentCreateIcon(): "documentCreateIcon",
  387. theme.DocumentIcon(): "documentIcon",
  388. theme.DocumentPrintIcon(): "documentPrintIcon",
  389. theme.DocumentSaveIcon(): "documentSaveIcon",
  390. theme.DownloadIcon(): "downloadIcon",
  391. theme.ErrorIcon(): "errorIcon",
  392. theme.FileApplicationIcon(): "fileApplicationIcon",
  393. theme.FileAudioIcon(): "fileAudioIcon",
  394. theme.FileIcon(): "fileIcon",
  395. theme.FileImageIcon(): "fileImageIcon",
  396. theme.FileTextIcon(): "fileTextIcon",
  397. theme.FileVideoIcon(): "fileVideoIcon",
  398. theme.FolderIcon(): "folderIcon",
  399. theme.FolderNewIcon(): "folderNewIcon",
  400. theme.FolderOpenIcon(): "folderOpenIcon",
  401. theme.FyneLogo(): "fyneLogo",
  402. theme.HelpIcon(): "helpIcon",
  403. theme.HistoryIcon(): "historyIcon",
  404. theme.HomeIcon(): "homeIcon",
  405. theme.InfoIcon(): "infoIcon",
  406. theme.MailAttachmentIcon(): "mailAttachementIcon",
  407. theme.MailComposeIcon(): "mailComposeIcon",
  408. theme.MailForwardIcon(): "mailForwardIcon",
  409. theme.MailReplyAllIcon(): "mailReplyAllIcon",
  410. theme.MailReplyIcon(): "mailReplyIcon",
  411. theme.MailSendIcon(): "mailSendIcon",
  412. theme.MediaFastForwardIcon(): "mediaFastForwardIcon",
  413. theme.MediaFastRewindIcon(): "mediaFastRewindIcon",
  414. theme.MediaPauseIcon(): "mediaPauseIcon",
  415. theme.MediaPlayIcon(): "mediaPlayIcon",
  416. theme.MediaRecordIcon(): "mediaRecordIcon",
  417. theme.MediaReplayIcon(): "mediaReplayIcon",
  418. theme.MediaSkipNextIcon(): "mediaSkipNextIcon",
  419. theme.MediaSkipPreviousIcon(): "mediaSkipPreviousIcon",
  420. theme.MenuDropDownIcon(): "menuDropDownIcon",
  421. theme.MenuDropUpIcon(): "menuDropUpIcon",
  422. theme.MenuExpandIcon(): "menuExpandIcon",
  423. theme.MenuIcon(): "menuIcon",
  424. theme.MoveDownIcon(): "moveDownIcon",
  425. theme.MoveUpIcon(): "moveUpIcon",
  426. theme.NavigateBackIcon(): "navigateBackIcon",
  427. theme.NavigateNextIcon(): "navigateNextIcon",
  428. theme.QuestionIcon(): "questionIcon",
  429. theme.RadioButtonCheckedIcon(): "radioButtonCheckedIcon",
  430. theme.RadioButtonIcon(): "radioButtonIcon",
  431. theme.SearchIcon(): "searchIcon",
  432. theme.SearchReplaceIcon(): "searchReplaceIcon",
  433. theme.SettingsIcon(): "settingsIcon",
  434. theme.StorageIcon(): "storageIcon",
  435. theme.ViewFullScreenIcon(): "viewFullScreenIcon",
  436. theme.ViewRefreshIcon(): "viewRefreshIcon",
  437. theme.ViewRestoreIcon(): "viewRestoreIcon",
  438. theme.VisibilityIcon(): "visibilityIcon",
  439. theme.VisibilityOffIcon(): "visibilityOffIcon",
  440. theme.VolumeDownIcon(): "volumeDownIcon",
  441. theme.VolumeMuteIcon(): "volumeMuteIcon",
  442. theme.VolumeUpIcon(): "volumeUpIcon",
  443. theme.WarningIcon(): "warningIcon",
  444. theme.ZoomFitIcon(): "zoomFitIcon",
  445. theme.ZoomInIcon(): "zoomInIcon",
  446. theme.ZoomOutIcon(): "zoomOutIcon",
  447. }[rsc]
  448. }
  449. func sortedKeys(m map[string]*string) []string {
  450. keys := make([]string, 0, len(m))
  451. for k := range m {
  452. keys = append(keys, k)
  453. }
  454. sort.Strings(keys)
  455. return keys
  456. }