diff --git a/Markdown.go b/Markdown.go index 9aa407fc..0f55f167 100644 --- a/Markdown.go +++ b/Markdown.go @@ -1,127 +1,186 @@ -//nolint:gocritic,govet,wsl,revive // this file is TODO. We don't want commentedOutCode lint issues here. package giu +import ( + "image" + "image/color" + "net/http" + "strings" + "time" + + "github.com/AllenDang/cimgui-go/backend" + "github.com/AllenDang/cimgui-go/imgui" + "github.com/AllenDang/cimgui-go/immarkdown" +) + +type markdownState struct { + cfg immarkdown.MarkdownConfig + images map[string]immarkdown.MarkdownImageData +} + +func (m *markdownState) Dispose() { + // noop +} + // MarkdownWidget implements DearImGui markdown extension // https://github.com/juliettef/imgui_markdown // It is like LabelWidget but with md formatting. -// TODO: re-implement this. type MarkdownWidget struct { - md *string - linkCb func(url string) - // headers []imgui.MarkdownHeaderData + md string + id ID + headers [3]immarkdown.MarkdownHeadingFormat } -// Markdown creates new markdown widget. -func Markdown(md *string) *MarkdownWidget { - panic("MarkdownWidget is not implemented yet!") +func (m *MarkdownWidget) getState() *markdownState { + if s := GetState[markdownState](Context, m.id); s != nil { + return s + } + + newState := m.newState() + SetState[markdownState](Context, m.id, newState) - return &MarkdownWidget{ - md: md, - linkCb: OpenURL, + return newState +} + +func (m *MarkdownWidget) newState() *markdownState { + cfg := immarkdown.NewEmptyMarkdownConfig() + fmtCb := immarkdown.MarkdownFormalCallback(func(data *immarkdown.MarkdownFormatInfo, start bool) { + immarkdown.DefaultMarkdownFormatCallback(*data, start) + }) + + cfg.SetFormatCallback(&fmtCb) + + imgCb := immarkdown.MarkdownImageCallback(func(data immarkdown.MarkdownLinkCallbackData) immarkdown.MarkdownImageData { + link := data.Link()[:data.LinkLength()] // this is because imgui_markdown returns the whole text starting on link and returns link length (for some reason) + if existing, ok := m.getState().images[link]; ok { + return existing + } + + result := mdLoadImage(link) + m.getState().images[link] = result + + return result + }) + + cfg.SetImageCallback(&imgCb) + + return &markdownState{ + cfg: *cfg, + images: make(map[string]immarkdown.MarkdownImageData), } } +// Markdown creates new markdown widget. +func Markdown(md string) *MarkdownWidget { + return (&MarkdownWidget{ + md: md, + id: GenAutoID("MarkdownWidget"), + headers: [3]immarkdown.MarkdownHeadingFormat{ + *immarkdown.NewEmptyMarkdownHeadingFormat(), + *immarkdown.NewEmptyMarkdownHeadingFormat(), + *immarkdown.NewEmptyMarkdownHeadingFormat(), + }, + }).OnLink(OpenURL) +} + // OnLink sets another than default link callback. +// NOTE: due to cimgui-go's limitation https://github.com/AllenDang/cimgui-go?tab=readme-ov-file#callbacks +// we clear MarkdownLinkCallback pool every frame. No further action from you should be required (just feel informed). +// ref (*MasterWindow).beforeRender. func (m *MarkdownWidget) OnLink(cb func(url string)) *MarkdownWidget { - m.linkCb = cb + igCb := immarkdown.MarkdownLinkCallback(func(data immarkdown.MarkdownLinkCallbackData) { + link := data.Link()[:data.LinkLength()] + cb(link) + }) + + m.getState().cfg.SetLinkCallback(&igCb) + return m } // Header sets header formatting // NOTE: level (counting from 0!) is header level. (for instance, header `# H1` will have level 0). +// NOTE: since cimgui-go there are only 3 levels (so level < 3 here). This will panic if level >= 3! +// TODO: it actually doesn't work. func (m *MarkdownWidget) Header(level int, font *FontInfo, separator bool) *MarkdownWidget { - // ensure if header data are at least as long as level - // if m.headers == nil { - // m.headers = make([]imgui.MarkdownHeaderData, level) - //} + // ensure level is in range + Assert(level < 3, "MarkdownWidget", "Header", "Header level must be less than 3!") - // if level <= len(m.headers) { - // m.headers = append(m.headers, make([]imgui.MarkdownHeaderData, len(m.headers)-level+1)...) - //} + m.headers[level] = *immarkdown.NewEmptyMarkdownHeadingFormat() - // if font != nil { - // if f, ok := Context.FontAtlas.extraFontMap[font.String()]; ok { - // m.headers[level].Font = *f - // } - //} + if font != nil { + if f, ok := Context.FontAtlas.extraFontMap[font.String()]; ok { + m.headers[level].SetFont(f) + } + } - // m.headers[level].HasSeparator = separator + m.headers[level].SetSeparator(separator) + + state := m.getState() + state.cfg.SetHeadingFormats(&m.headers) return m } // Build implements Widget interface. func (m *MarkdownWidget) Build() { - // imgui.Markdown(Context.FontAtlas.RegisterStringPointer(m.md), m.linkCb, loadImage, m.headers) + state := m.getState() + immarkdown.Markdown( + Context.FontAtlas.RegisterString(m.md), + uint64(len(m.md)), + state.cfg, + ) } -//func loadImage(path string) imgui.MarkdownImageData { -// var img *image.RGBA -// -// var err error -// -// switch { -// case strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://"): -// Load image from url -//client := &http.Client{Timeout: 5 * time.Second} -//resp, respErr := client.Get(path) -// -//if respErr != nil { -// return imgui.MarkdownImageData{} -//} -// -//defer func() { -// closeErr := resp.Body.Close() -// Assert((closeErr == nil), "MarkdownWidget", "loadImage", "Could not close http request!") -//}() -// -//rgba, _, imgErr := image.Decode(resp.Body) -//if imgErr != nil { -// return imgui.MarkdownImageData{} -//} -// -//img = ImageToRgba(rgba) -//default: -// img, err = LoadImage(path) -// if err != nil { -// return imgui.MarkdownImageData{} -// } -//} -// -//size := img.Bounds() -// -//nolint:gocritic // TODO/BUG: figure out, why it doesn't work as expected and consider -//if current workaround is save -///* -// tex := &Texture{} -// NewTextureFromRgba(img, func(t *Texture) { -// fmt.Println("creating texture") -// tex.id = t.id -// }) -//*/ -// -//var id imgui.TextureID -// -//mainthread.Call(func() { -// var err error -// id, err = Context.renderer.LoadImage(img) -// if err != nil { -// return -// } -//}) -// -//return imgui.MarkdownImageData{ -// TextureID: &id, -// Scale: true, -// Size: imgui.Vec2{ -// X: float32(size.Dx()), -// Y: float32(size.Dy()), -// }, -// UseLinkCallback: true, -// default values -//Uv0: ToVec2(image.Point{0, 0}), -//Uv1: ToVec2(image.Point{1, 1}), -//TintColor: ToVec4Color(color.RGBA{255, 255, 255, 255}), -//BorderColor: ToVec4Color(color.RGBA{0, 0, 0, 0}), -//} -//} +func mdLoadImage(path string) immarkdown.MarkdownImageData { + var ( + img *image.RGBA + err error + ) + + switch { + case strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://"): + // Load image from url + client := &http.Client{Timeout: 5 * time.Second} + + resp, respErr := client.Get(path) + if respErr != nil { + return *immarkdown.NewEmptyMarkdownImageData() + } + + defer func() { + closeErr := resp.Body.Close() + Assert((closeErr == nil), "MarkdownWidget", "mdLoadImage", "Could not close http request!") + }() + + rgba, _, imgErr := image.Decode(resp.Body) + if imgErr != nil { + return *immarkdown.NewEmptyMarkdownImageData() + } + + img = ImageToRgba(rgba) + default: + img, err = LoadImage(path) + if err != nil { + return *immarkdown.NewEmptyMarkdownImageData() + } + } + + size := img.Bounds() + id := backend.NewTextureFromRgba(img).ID + + result := immarkdown.NewEmptyMarkdownImageData() + result.SetUsertextureid(id) + result.SetSize(imgui.Vec2{ + X: float32(size.Dx()), + Y: float32(size.Dy()), + }) + result.SetUseLinkCallback(true) + result.SetUv0(ToVec2(image.Point{0, 0})) + result.SetUv1(ToVec2(image.Point{1, 1})) + result.SetTintcol(ToVec4Color(color.RGBA{255, 255, 255, 255})) + result.SetBordercol(ToVec4Color(color.RGBA{0, 0, 0, 0})) + + result.SetIsValid(true) + + return *result +} diff --git a/MasterWindow.go b/MasterWindow.go index 95089b7e..bd43c138 100644 --- a/MasterWindow.go +++ b/MasterWindow.go @@ -10,6 +10,7 @@ import ( "github.com/AllenDang/cimgui-go/backend/glfwbackend" "github.com/AllenDang/cimgui-go/imgui" "github.com/AllenDang/cimgui-go/imguizmo" + "github.com/AllenDang/cimgui-go/immarkdown" "github.com/AllenDang/cimgui-go/imnodes" "github.com/AllenDang/cimgui-go/implot" "golang.org/x/image/colornames" @@ -201,6 +202,10 @@ func (w *MasterWindow) sizeChange(_, _ int) { } func (w *MasterWindow) beforeRender() { + // Clean callbacks + // see https://github.com/AllenDang/cimgui-go?tab=readme-ov-file#callbacks + immarkdown.ClearMarkdownLinkCallbackPool() + Context.FontAtlas.rebuildFontAtlas() // process texture load requests diff --git a/examples/markdown/markdown.go b/examples/markdown/markdown.go index a6b9bdc4..0afe22f1 100644 --- a/examples/markdown/markdown.go +++ b/examples/markdown/markdown.go @@ -3,55 +3,51 @@ package main import ( - "strings" - "github.com/AllenDang/giu" ) +const defaultMd = ` +Wrapping: +Text wraps automatically. To add a new line, use 'Return'. + +Headers: +# H1 +## H2 +### H3 + +Emphasis: +*emphasis* +_emphasis_ +**strong emphasis** +__strong emphasis__ + +Indents: +On a new line, at the start of the line, add two spaces per indent. + Indent level 1 + Indent level 2 + +Unordered lists: +On a new line, at the start of the line, add two spaces, an asterisks and a space. +For nested lists, add two additional spaces in front of the asterisk per list level increment. + * Unordered List level 1 + * Unordered List level 2 + +Link: +Here is [a link to some cool website!](https://github.com/AllenDang/giu) you must click it! +Image: +![gopher image](./gopher.png) +![gopher image link](https://raw.githubusercontent.com/AllenDang/giu/master/examples/loadimage/gopher.png) + +Horizontal Rule: +*** +___ +` + var ( - markdown = getExampleMarkdownText() + markdown = defaultMd splitLayoutPos float32 = 320 ) -func getExampleMarkdownText() string { - return strings.Join([]string{ - "Wrapping:", - "Text wraps automatically. To add a new line, use 'Return'.", - "", - "Headers:", - "# H1", - "## H2", - "### H3", - "", - "Emphasis:", - "*emphasis*", - "_emphasis_", - "**strong emphasis**", - "__strong emphasis__", - "", - "Indents:", - "On a new line, at the start of the line, add two spaces per indent.", - " Indent level 1", - " Indent level 2", - "", - "Unordered lists:", - "On a new line, at the start of the line, add two spaces, an asterisks and a space.", - "For nested lists, add two additional spaces in front of the asterisk per list level increment.", - " * Unordered List level 1", - " * Unordered List level 2", - "", - "Link:", - "Here is [a link to some cool website!](https://github.com/AllenDang/giu) you must click it!", - "Image:", - "![gopher image](./gopher.png)", - "![gopher image link](https://raw.githubusercontent.com/AllenDang/giu/master/examples/loadimage/gopher.png)", - "", - "Horizontal Rule:", - "***", - "___", - }, "\n") -} - func loop() { giu.SingleWindow().Layout( giu.SplitLayout(giu.DirectionHorizontal, &splitLayoutPos, @@ -59,7 +55,7 @@ func loop() { giu.Row( giu.Label("Markdown Edition:"), giu.Button("Reset").OnClick(func() { - markdown = getExampleMarkdownText() + markdown = defaultMd }), ), giu.Custom(func() { @@ -67,7 +63,7 @@ func loop() { giu.InputTextMultiline(&markdown).Size(availableW, availableH).Build() }), }, - giu.Markdown(&markdown). + giu.Markdown(markdown). Header(0, (giu.Context.FontAtlas.GetDefaultFonts())[0].SetSize(28), true). Header(1, (giu.Context.FontAtlas.GetDefaultFonts())[0].SetSize(26), false). Header(2, nil, true), diff --git a/go.mod b/go.mod index 5000f4e0..c60129e5 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/AllenDang/giu -go 1.23.2 +go 1.23.3 require ( - github.com/AllenDang/cimgui-go v1.1.1-0.20241105214716-82a5c1ae9712 + github.com/AllenDang/cimgui-go v1.2.0 github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8 github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 github.com/gucio321/glm-go v0.0.0-20241029220517-e1b5a3e011c8 diff --git a/go.sum b/go.sum index 6e3aca82..e4c1d782 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ github.com/AllenDang/cimgui-go v1.1.1-0.20241105214716-82a5c1ae9712 h1:Qog9ZG14NbkuzbqzwT7zX9u+yi1fTU8ZIlesefYmc40= github.com/AllenDang/cimgui-go v1.1.1-0.20241105214716-82a5c1ae9712/go.mod h1:/gM0PosnwlgK0Fc3UFALXWPEIu+cW2DC3YU0Z4EwV2Y= +github.com/AllenDang/cimgui-go v1.1.1-0.20241110184948-72c467a7f306 h1:B0zIFc0Y0kp5kYlI0gtZXh10bIhtWy0ClDzexJlCnxY= +github.com/AllenDang/cimgui-go v1.1.1-0.20241110184948-72c467a7f306/go.mod h1:/gM0PosnwlgK0Fc3UFALXWPEIu+cW2DC3YU0Z4EwV2Y= +github.com/AllenDang/cimgui-go v1.1.1-0.20241111141409-85e6553b3257 h1:Mel/E72ffrPxH+FAYgKTTIkbzRFfia6Dp8W+DvuTFk8= +github.com/AllenDang/cimgui-go v1.1.1-0.20241111141409-85e6553b3257/go.mod h1:KT0QhbfG00LVdgN/eOGhnrSSG8lMfdBvYmZJCBgp2JM= +github.com/AllenDang/cimgui-go v1.1.1-0.20241111163245-8c039e187bcc h1:z19BKKTjz+yJtaXGiu2xsziMuDt7k1td3v17kauJHqE= +github.com/AllenDang/cimgui-go v1.1.1-0.20241111163245-8c039e187bcc/go.mod h1:KT0QhbfG00LVdgN/eOGhnrSSG8lMfdBvYmZJCBgp2JM= +github.com/AllenDang/cimgui-go v1.2.0 h1:xlsBNlGW2n4X6WYi0B84iOoAYWQT+iJFKWM2iZvpzNI= +github.com/AllenDang/cimgui-go v1.2.0/go.mod h1:KT0QhbfG00LVdgN/eOGhnrSSG8lMfdBvYmZJCBgp2JM= github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8 h1:dKZMqib/yUDoCFigmz2agG8geZ/e3iRq304/KJXqKyw= github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8/go.mod h1:b4uuDd0s6KRIPa84cEEchdQ9ICh7K0OryZHbSzMca9k= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=