ImageWidgets.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. package giu
  2. import (
  3. ctx "context"
  4. "fmt"
  5. "image"
  6. "image/color"
  7. "net/http"
  8. "time"
  9. "github.com/AllenDang/imgui-go"
  10. )
  11. var _ Widget = &ImageWidget{}
  12. // ImageWidget adds an image.
  13. // NOTE: ImageWidget is going to be deprecated. ImageWithRGBAWidget
  14. // should be used instead, however, because it is a native
  15. // imgui's solution it is still there.
  16. type ImageWidget struct {
  17. texture *Texture
  18. width float32
  19. height float32
  20. uv0, uv1 image.Point
  21. tintColor, borderColor color.Color
  22. onClick func()
  23. }
  24. // Image adds an image from giu.Texture.
  25. func Image(texture *Texture) *ImageWidget {
  26. return &ImageWidget{
  27. texture: texture,
  28. width: 100,
  29. height: 100,
  30. uv0: image.Point{X: 0, Y: 0},
  31. uv1: image.Point{X: 1, Y: 1},
  32. tintColor: color.RGBA{255, 255, 255, 255},
  33. borderColor: color.RGBA{0, 0, 0, 0},
  34. }
  35. }
  36. // Uv allows to specify uv parameters.
  37. func (i *ImageWidget) Uv(uv0, uv1 image.Point) *ImageWidget {
  38. i.uv0, i.uv1 = uv0, uv1
  39. return i
  40. }
  41. // TintColor sets image's tint color.
  42. func (i *ImageWidget) TintColor(tintColor color.Color) *ImageWidget {
  43. i.tintColor = tintColor
  44. return i
  45. }
  46. // BorderCol sets color of the border.
  47. func (i *ImageWidget) BorderCol(borderColor color.Color) *ImageWidget {
  48. i.borderColor = borderColor
  49. return i
  50. }
  51. // OnClick adds on-click-callback.
  52. func (i *ImageWidget) OnClick(cb func()) *ImageWidget {
  53. i.onClick = cb
  54. return i
  55. }
  56. // Size sets image size.
  57. func (i *ImageWidget) Size(width, height float32) *ImageWidget {
  58. // Size image with DPI scaling
  59. factor := Context.GetPlatform().GetContentScale()
  60. i.width, i.height = width*factor, height*factor
  61. return i
  62. }
  63. // Build implements Widget interface.
  64. func (i *ImageWidget) Build() {
  65. size := imgui.Vec2{X: i.width, Y: i.height}
  66. rect := imgui.ContentRegionAvail()
  67. if size.X == -1 {
  68. size.X = rect.X
  69. }
  70. if size.Y == -1 {
  71. size.Y = rect.Y
  72. }
  73. if i.texture == nil || i.texture.id == 0 {
  74. Dummy(size.X, size.Y).Build()
  75. return
  76. }
  77. // trick: detect click event
  78. if i.onClick != nil && IsMouseClicked(MouseButtonLeft) {
  79. cursorPos := GetCursorScreenPos()
  80. mousePos := GetMousePos()
  81. mousePos.Add(cursorPos)
  82. if cursorPos.X <= mousePos.X && cursorPos.Y <= mousePos.Y &&
  83. cursorPos.X+int(i.width) >= mousePos.X && cursorPos.Y+int(i.height) >= mousePos.Y {
  84. i.onClick()
  85. }
  86. }
  87. imgui.ImageV(i.texture.id, size, ToVec2(i.uv0), ToVec2(i.uv1), ToVec4Color(i.tintColor), ToVec4Color(i.borderColor))
  88. }
  89. type imageState struct {
  90. loading bool
  91. failure bool
  92. cancel ctx.CancelFunc
  93. texture *Texture
  94. }
  95. // Dispose cleans imageState (implements Disposable interface).
  96. func (is *imageState) Dispose() {
  97. is.texture = nil
  98. // Cancel ongoing image downloaidng
  99. if is.loading && is.cancel != nil {
  100. is.cancel()
  101. }
  102. }
  103. var _ Widget = &ImageWithRgbaWidget{}
  104. // ImageWithRgbaWidget wrapps ImageWidget.
  105. // It is more useful because it doesn't make you to care about
  106. // imgui textures. You can just pass golang-native image.Image and
  107. // display it in giu.
  108. type ImageWithRgbaWidget struct {
  109. id string
  110. rgba image.Image
  111. img *ImageWidget
  112. }
  113. // ImageWithRgba creates ImageWithRgbaWidget.
  114. func ImageWithRgba(rgba image.Image) *ImageWithRgbaWidget {
  115. return &ImageWithRgbaWidget{
  116. id: GenAutoID("ImageWithRgba"),
  117. rgba: rgba,
  118. img: Image(nil),
  119. }
  120. }
  121. // Size sets image's size.
  122. func (i *ImageWithRgbaWidget) Size(width, height float32) *ImageWithRgbaWidget {
  123. i.img.Size(width, height)
  124. return i
  125. }
  126. // OnClick sets click callback.
  127. func (i *ImageWithRgbaWidget) OnClick(cb func()) *ImageWithRgbaWidget {
  128. i.img.OnClick(cb)
  129. return i
  130. }
  131. // Build implements Widget interface.
  132. func (i *ImageWithRgbaWidget) Build() {
  133. if i.rgba != nil {
  134. var imgState *imageState
  135. if state := Context.GetState(i.id); state == nil {
  136. imgState = &imageState{}
  137. Context.SetState(i.id, imgState)
  138. NewTextureFromRgba(i.rgba, func(tex *Texture) {
  139. imgState.texture = tex
  140. })
  141. } else {
  142. var isOk bool
  143. imgState, isOk = state.(*imageState)
  144. Assert(isOk, "ImageWithRgbaWidget", "Build", "unexpected type of widget's state recovered")
  145. }
  146. i.img.texture = imgState.texture
  147. }
  148. i.img.Build()
  149. }
  150. var _ Widget = &ImageWithFileWidget{}
  151. // ImageWithFileWidget allows to display an image directly
  152. // from .png file.
  153. // NOTE: Be aware that project using this solution may not be portable
  154. // because files are not included in executable binaries!
  155. // You may want to use "embed" package and ImageWithRgba instead.
  156. type ImageWithFileWidget struct {
  157. id string
  158. imgPath string
  159. img *ImageWidget
  160. }
  161. // ImageWithFile constructs a new ImageWithFileWidget.
  162. func ImageWithFile(imgPath string) *ImageWithFileWidget {
  163. return &ImageWithFileWidget{
  164. id: fmt.Sprintf("ImageWithFile_%s", imgPath),
  165. imgPath: imgPath,
  166. img: Image(nil),
  167. }
  168. }
  169. // Size sets image's size.
  170. func (i *ImageWithFileWidget) Size(width, height float32) *ImageWithFileWidget {
  171. i.img.Size(width, height)
  172. return i
  173. }
  174. // OnClick sets click callback.
  175. func (i *ImageWithFileWidget) OnClick(cb func()) *ImageWithFileWidget {
  176. i.img.OnClick(cb)
  177. return i
  178. }
  179. // Build implements Widget interface.
  180. func (i *ImageWithFileWidget) Build() {
  181. imgState := &imageState{}
  182. if state := Context.GetState(i.id); state == nil {
  183. // Prevent multiple invocation to LoadImage.
  184. Context.SetState(i.id, imgState)
  185. img, err := LoadImage(i.imgPath)
  186. if err == nil {
  187. NewTextureFromRgba(img, func(tex *Texture) {
  188. imgState.texture = tex
  189. })
  190. }
  191. } else {
  192. var isOk bool
  193. imgState, isOk = state.(*imageState)
  194. Assert(isOk, "ImageWithFileWidget", "Build", "wrong type of widget's state got")
  195. }
  196. i.img.texture = imgState.texture
  197. i.img.Build()
  198. }
  199. var _ Widget = &ImageWithURLWidget{}
  200. // ImageWithURLWidget allows to display an image using
  201. // an URL as image source.
  202. type ImageWithURLWidget struct {
  203. id string
  204. imgURL string
  205. downloadTimeout time.Duration
  206. whenLoading Layout
  207. whenFailure Layout
  208. onReady func()
  209. onFailure func(error)
  210. img *ImageWidget
  211. }
  212. // ImageWithURL creates ImageWithURLWidget.
  213. func ImageWithURL(url string) *ImageWithURLWidget {
  214. return &ImageWithURLWidget{
  215. id: fmt.Sprintf("ImageWithURL_%s", url),
  216. imgURL: url,
  217. downloadTimeout: 10 * time.Second,
  218. whenLoading: Layout{Dummy(100, 100)},
  219. whenFailure: Layout{Dummy(100, 100)},
  220. img: Image(nil),
  221. }
  222. }
  223. // OnReady sets event trigger when image is downloaded and ready to display.
  224. func (i *ImageWithURLWidget) OnReady(onReady func()) *ImageWithURLWidget {
  225. i.onReady = onReady
  226. return i
  227. }
  228. // OnFailure sets event trigger when image failed to download/load.
  229. func (i *ImageWithURLWidget) OnFailure(onFailure func(error)) *ImageWithURLWidget {
  230. i.onFailure = onFailure
  231. return i
  232. }
  233. // OnClick sets click callback.
  234. func (i *ImageWithURLWidget) OnClick(cb func()) *ImageWithURLWidget {
  235. i.img.OnClick(cb)
  236. return i
  237. }
  238. // Timeout sets download timeout.
  239. func (i *ImageWithURLWidget) Timeout(downloadTimeout time.Duration) *ImageWithURLWidget {
  240. i.downloadTimeout = downloadTimeout
  241. return i
  242. }
  243. // Size sets image's size.
  244. func (i *ImageWithURLWidget) Size(width, height float32) *ImageWithURLWidget {
  245. i.img.Size(width, height)
  246. return i
  247. }
  248. // LayoutForLoading allows to set layout rendered while loading an image.
  249. func (i *ImageWithURLWidget) LayoutForLoading(widgets ...Widget) *ImageWithURLWidget {
  250. i.whenLoading = Layout(widgets)
  251. return i
  252. }
  253. // LayoutForFailure allows to specify layout when image failed to download.
  254. func (i *ImageWithURLWidget) LayoutForFailure(widgets ...Widget) *ImageWithURLWidget {
  255. i.whenFailure = Layout(widgets)
  256. return i
  257. }
  258. // Build implements Widget interface.
  259. func (i *ImageWithURLWidget) Build() {
  260. imgState := &imageState{}
  261. if state := Context.GetState(i.id); state == nil {
  262. Context.SetState(i.id, imgState)
  263. // Prevent multiple invocation to download image.
  264. downloadContext, cancalFunc := ctx.WithCancel(ctx.Background())
  265. Context.SetState(i.id, &imageState{loading: true, cancel: cancalFunc})
  266. errorFn := func(err error) {
  267. Context.SetState(i.id, &imageState{failure: true})
  268. // Trigger onFailure event
  269. if i.onFailure != nil {
  270. i.onFailure(err)
  271. }
  272. }
  273. go func() {
  274. // Load image from url
  275. client := &http.Client{Timeout: i.downloadTimeout}
  276. req, err := http.NewRequestWithContext(downloadContext, "GET", i.imgURL, http.NoBody)
  277. if err != nil {
  278. errorFn(err)
  279. return
  280. }
  281. resp, err := client.Do(req)
  282. if err != nil {
  283. errorFn(err)
  284. return
  285. }
  286. defer func() {
  287. if closeErr := resp.Body.Close(); closeErr != nil {
  288. errorFn(closeErr)
  289. }
  290. }()
  291. img, _, err := image.Decode(resp.Body)
  292. if err != nil {
  293. errorFn(err)
  294. return
  295. }
  296. rgba := ImageToRgba(img)
  297. NewTextureFromRgba(rgba, func(tex *Texture) {
  298. Context.SetState(i.id, &imageState{
  299. loading: false,
  300. failure: false,
  301. texture: tex,
  302. })
  303. })
  304. // Trigger onReady event
  305. if i.onReady != nil {
  306. i.onReady()
  307. }
  308. }()
  309. } else {
  310. var isOk bool
  311. imgState, isOk = state.(*imageState)
  312. Assert(isOk, "ImageWithURLWidget", "Build", "wrong type of widget's state recovered.")
  313. }
  314. switch {
  315. case imgState.failure:
  316. i.whenFailure.Build()
  317. case imgState.loading:
  318. i.whenLoading.Build()
  319. default:
  320. i.img.texture = imgState.texture
  321. i.img.Build()
  322. }
  323. }