├── .gitignore ├── FyneApp.toml ├── Icon.png ├── LICENSE ├── README.md ├── config.go ├── export.go ├── export_test.go ├── file.go ├── go.mod ├── go.sum ├── img ├── screenshot-code.png └── screenshot.png ├── layout.go ├── main.go ├── main.gui.go ├── matrix.json ├── parse.go ├── present.go ├── slide.go ├── slidelayout.go ├── slides.go ├── slidetheme.go ├── slideutil.go ├── slidewidgets.go ├── style.go ├── testdata └── export.pdf └── widgets.go /.gitignore: -------------------------------------------------------------------------------- 1 | slydes 2 | 3 | .idea 4 | 5 | -------------------------------------------------------------------------------- /FyneApp.toml: -------------------------------------------------------------------------------- 1 | Description = "A slideshow app using Fyne and markdown" 2 | 3 | [Details] 4 | Icon = "Icon.png" 5 | Name = "Slydes" 6 | ID = "xyz.andy.slydes" 7 | Build = 6 8 | -------------------------------------------------------------------------------- /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andydotxyz/slydes/74deb00b8a6c613269010d587f8a90147f4c6f93/Icon.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2023 Andrew Williams 2 | All rights reserved. 3 | 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Fyne.io nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
4 | 5 | # Slydes 6 | 7 | A presentation app using Fyne and Markdown 8 | 9 |  10 | 11 | ## Format 12 | 13 | Each presentation is a simple markdown file. 14 | It also supports toml front matter for configuration, such as theme. 15 | 16 | ```md 17 | +++ 18 | theme = "matrix" 19 | +++ 20 | 21 | # Heading 22 | ## Subheading 23 | 24 | --- 25 | 26 | # Slide 2 27 | 28 | Content for slide 29 | 30 | --- 31 | 32 | # Bullets 33 | 34 | * First 35 | * Second 36 | * Third 37 | 38 | ``` 39 | 40 | ## Code styling 41 | 42 | You can include styled text using fenced or indented code blocks. 43 | When using the fenced (\`\`\`) block you can specify the language too, as follows: 44 | 45 | ```go 46 | func hello() { 47 | log.Println("hello") 48 | } 49 | ``` 50 | 51 |  52 | 53 | Thanks to the excellent [goshot](https://github.com/watzon/goshot) project for 54 | providing the code rendering support! -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type config struct { 4 | Theme string `toml:"theme"` 5 | } 6 | -------------------------------------------------------------------------------- /export.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "image/color" 6 | "image/jpeg" 7 | "image/png" 8 | "io" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/go-pdf/fpdf" 16 | 17 | "fyne.io/fyne/v2" 18 | "fyne.io/fyne/v2/canvas" 19 | "fyne.io/fyne/v2/test" 20 | ) 21 | 22 | var imgID int // each image needs a unique name 23 | 24 | func export(s *slides, w io.Writer) error { 25 | doc := fpdf.NewCustom(&fpdf.InitType{ 26 | Size: fpdf.SizeType{Wd: 1600, Ht: 900}, 27 | UnitStr: fpdf.UnitPoint, 28 | }) 29 | pageWidth, totalHeight := doc.GetPageSize() 30 | pageHeight := pageWidth * (9.0 / 16.0) 31 | for _, item := range s.items { 32 | doc.AddPage() 33 | 34 | s := newSlide(item, s) 35 | s.Resize(fyne.NewSize(float32(pageWidth), float32(pageHeight))) 36 | err := renderObjectsToPDF(doc, s.content, fyne.Position{Y: float32(totalHeight-pageHeight) / 2}) 37 | if err != nil { 38 | fyne.LogError("Failed to encode the PDF", err) 39 | } 40 | } 41 | return doc.Output(w) 42 | } 43 | 44 | func renderObjectsToPDF(doc *fpdf.Fpdf, o fyne.CanvasObject, off fyne.Position) (err error) { 45 | switch c := o.(type) { 46 | case *fyne.Container: 47 | for _, o := range c.Objects { 48 | err2 := renderObjectsToPDF(doc, o, off.Add(c.Position())) 49 | if err == nil && err2 != nil { // propagate first error 50 | err = err2 51 | } 52 | } 53 | case fyne.Widget: 54 | for _, o := range test.WidgetRenderer(c).Objects() { 55 | err2 := renderObjectsToPDF(doc, o, off.Add(c.Position())) 56 | if err == nil && err2 != nil { // propagate first error 57 | err = err2 58 | } 59 | } 60 | case *canvas.Rectangle: 61 | x, y := c.Position().Add(off).Components() 62 | w, h := c.Size().Components() 63 | style := "" 64 | if c.FillColor != nil && c.FillColor != color.Transparent { 65 | style += "F" 66 | r, g, b, _ := c.FillColor.RGBA() 67 | doc.SetFillColor(int(r>>8), int(g>>8), int(b>>8)) 68 | } 69 | if c.StrokeWidth > 0 && c.StrokeColor != nil && c.StrokeColor != color.Transparent { 70 | style += "D" 71 | r, g, b, _ := c.StrokeColor.RGBA() 72 | doc.SetDrawColor(int(r), int(g), int(b)) 73 | doc.SetLineWidth(float64(c.StrokeWidth)) 74 | } 75 | doc.Rect(float64(x), float64(y), float64(w), float64(h), style) 76 | case *canvas.Text: 77 | r, g, b, _ := c.Color.RGBA() 78 | doc.SetTextColor(int(r>>8), int(g>>8), int(b>>8)) 79 | 80 | x, y := c.Position().Add(off).Components() 81 | size, base := fyne.CurrentApp().Driver().RenderedTextSize(c.Text, c.TextSize, c.TextStyle, c.FontSource) 82 | style := "" 83 | if c.TextStyle.Bold { 84 | style += "B" 85 | } 86 | if c.TextStyle.Italic { 87 | style += "I" 88 | } 89 | 90 | if c.TextStyle.Monospace { 91 | doc.SetFont("Courier", style, float64(c.TextSize)) 92 | } else { 93 | doc.SetFont("Helvetica", style, float64(c.TextSize)) 94 | } 95 | 96 | w := c.Size().Width 97 | switch c.Alignment { 98 | case fyne.TextAlignCenter: 99 | x += (w - size.Width) / 2 100 | case fyne.TextAlignTrailing: 101 | x += w - size.Width 102 | } 103 | topPad := (c.Size().Height - size.Height) / 2 104 | if topPad < 0 { // if size was accidentally too small! 105 | topPad = 0 106 | } 107 | 108 | doc.Text(float64(x), float64(y+base+topPad), c.Text) 109 | case *canvas.Circle: 110 | x, y := c.Position().Add(off).Components() 111 | w, h := c.Size().Components() 112 | style := "" 113 | if c.FillColor != nil && c.FillColor != color.Transparent { 114 | style += "F" 115 | r, g, b, _ := c.FillColor.RGBA() 116 | doc.SetFillColor(int(r>>8), int(g>>8), int(b>>8)) 117 | } 118 | if c.StrokeWidth > 0 && c.StrokeColor != nil && c.StrokeColor != color.Transparent { 119 | style += "D" 120 | r, g, b, _ := c.StrokeColor.RGBA() 121 | doc.SetDrawColor(int(r), int(g), int(b)) 122 | doc.SetLineWidth(float64(c.StrokeWidth)) 123 | } 124 | r := w / 2 125 | if h < w { 126 | r = h / 2 127 | } 128 | doc.Circle(float64(x+r), float64(y+r), float64(r), style) 129 | case *canvas.Image: 130 | ext := "" 131 | if c.File != "" { 132 | ext = strings.ToLower(filepath.Ext(c.File)) 133 | } else if c.Resource != nil { 134 | ext = strings.ToLower(filepath.Ext(c.Resource.Name())) 135 | } 136 | imgType := "PNG" 137 | if ext != "" && (ext == ".jpg" || ext == ".jpeg") { 138 | imgType = "JPEG" 139 | } 140 | size := c.Size() 141 | x, y := c.Position().Add(off).Components() 142 | w, h := size.Components() 143 | 144 | if c.FillMode == canvas.ImageFillContain { 145 | imageAspect := c.Aspect() 146 | viewAspect := size.Width / size.Height 147 | 148 | if viewAspect > imageAspect { 149 | w = size.Height * imageAspect 150 | x += (size.Width - w) / 2 151 | } else if viewAspect < imageAspect { 152 | h = size.Width / imageAspect 153 | y += (size.Height - h) / 2 154 | } 155 | } 156 | 157 | imgID++ 158 | name := strconv.Itoa(imgID) + ".png" // a unique name in case any filename collides 159 | if imgType == "JPEG" { 160 | name = name[:len(name)-3] + "jpeg" 161 | } 162 | var r io.Reader 163 | b := &bytes.Buffer{} 164 | if c.Image != nil { 165 | if imgType == "JPEG" { 166 | err = jpeg.Encode(b, c.Image, nil) 167 | } else { 168 | err = png.Encode(b, c.Image) 169 | } 170 | r = bytes.NewReader(b.Bytes()) 171 | } else if c.File != "" { 172 | r, err = os.Open(c.File) 173 | defer r.(io.ReadCloser).Close() 174 | } else if c.Resource != nil { 175 | r = bytes.NewReader(c.Resource.Content()) 176 | } 177 | 178 | // TODO image fill mode 179 | opts := fpdf.ImageOptions{ImageType: imgType} 180 | doc.RegisterImageOptionsReader(name, opts, r) 181 | doc.ImageOptions(name, float64(x), float64(y), float64(w), float64(h), false, opts, 0, "") 182 | default: 183 | log.Println("Missing handler for", c) 184 | } 185 | 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestExport(t *testing.T) { 10 | s := newSlides() 11 | s.parseSource(`# Slide 1 12 | ## Subhead 13 | 14 | --- 15 | # Slide 2 16 | 17 | Content! 18 | 19 | * Bullet 1 20 | * Bullet 2 21 | * Bullet 3 22 | 23 | --- 24 | 25 | # Icon 26 | 27 | left side 28 | 29 |  30 | `) 31 | buf := &bytes.Buffer{} 32 | err := export(s, buf) 33 | if err != nil { 34 | t.Error(err) 35 | return 36 | } 37 | 38 | golden, _ := os.ReadFile("testdata/export.pdf") 39 | data := buf.Bytes() 40 | if len(golden) != len(data) { 41 | t.Error("Wrong number of bytes in output") 42 | } 43 | 44 | end := len(golden) - 1024 // end of PDF not consistent on re-export 45 | for i, b := range golden { 46 | if i > end { 47 | break 48 | } 49 | if data[i] != b { 50 | t.Error("Wrong data at index", i, b, "!=", data[i]) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "path/filepath" 7 | "strings" 8 | 9 | "fyne.io/fyne/v2" 10 | "fyne.io/fyne/v2/dialog" 11 | "fyne.io/fyne/v2/storage" 12 | "fyne.io/fyne/v2/widget" 13 | ) 14 | 15 | func (g *gui) clearFile() { 16 | dialog.ShowConfirm("Clear content", "Are you sure you want to reset your slide content", func(ok bool) { 17 | if ok { 18 | g.s.uri = nil 19 | g.content.SetText("# Slide 1") 20 | } 21 | }, g.win) 22 | } 23 | 24 | func (g *gui) exportFile() { 25 | d := dialog.NewFileSave(func(w fyne.URIWriteCloser, err error) { 26 | if err != nil { 27 | dialog.ShowError(err, g.win) 28 | return 29 | } 30 | if w == nil { 31 | return 32 | } 33 | 34 | a := widget.NewActivity() 35 | d := dialog.NewCustomWithoutButtons("Printing", a, g.win) 36 | a.Start() 37 | d.Show() 38 | 39 | go func() { 40 | err = export(g.s, w) 41 | _ = w.Close() 42 | 43 | fyne.Do(func() { 44 | d.Hide() 45 | a.Stop() 46 | 47 | if err != nil { 48 | dialog.ShowError(err, g.win) 49 | } else { 50 | dialog.ShowInformation("Print to PDF", fmt.Sprintf("Printing to %s completed", w.URI().Name()), g.win) 51 | } 52 | }) 53 | }() 54 | }, g.win) 55 | 56 | name := "Untitled.md" 57 | if g.s.uri != nil { 58 | name = g.s.uri.Name() 59 | 60 | parent, err := storage.Parent(g.s.uri) 61 | if err != nil { 62 | fyne.LogError("Failed to get presentation parent", err) 63 | return 64 | } 65 | dir, err := storage.ListerForURI(parent) 66 | if err == nil { 67 | d.SetLocation(dir) 68 | } 69 | } 70 | name = strings.ReplaceAll(name, filepath.Ext(name), ".pdf") 71 | d.SetFileName(name) 72 | d.Show() 73 | } 74 | 75 | func (g *gui) openFile() { 76 | d := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { 77 | if err != nil { 78 | dialog.ShowError(err, g.win) 79 | return 80 | } 81 | if r == nil { 82 | return 83 | } 84 | 85 | data, err := io.ReadAll(r) 86 | _ = r.Close() 87 | 88 | if err != nil { 89 | dialog.ShowError(err, g.win) 90 | } else { 91 | g.s.uri = r.URI() 92 | g.content.SetText(string(data)) 93 | } 94 | }, g.win) 95 | d.SetFilter(storage.NewExtensionFileFilter([]string{".md"})) 96 | d.Show() 97 | } 98 | 99 | func (g *gui) saveFile() { 100 | if g.s.uri != nil { 101 | w, err := storage.Writer(g.s.uri) 102 | if err != nil { 103 | dialog.ShowError(err, g.win) 104 | return 105 | } 106 | 107 | _, err = w.Write([]byte(g.content.Text)) 108 | if err != nil { 109 | dialog.ShowError(err, g.win) 110 | } 111 | return 112 | } 113 | 114 | dialog.ShowFileSave(func(w fyne.URIWriteCloser, err error) { 115 | if err != nil { 116 | dialog.ShowError(err, g.win) 117 | return 118 | } 119 | if w == nil { 120 | return 121 | } 122 | 123 | _, err = w.Write([]byte(g.content.Text)) 124 | if err != nil { 125 | dialog.ShowError(err, g.win) 126 | } 127 | g.s.uri = w.URI() 128 | }, g.win) 129 | } 130 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module slydes 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | fyne.io/fyne/v2 v2.6.0-rc1 7 | github.com/BurntSushi/toml v1.4.0 8 | github.com/alecthomas/chroma/v2 v2.14.0 9 | github.com/go-pdf/fpdf v0.8.0 10 | github.com/watzon/goshot v0.7.1 11 | github.com/yuin/goldmark v1.7.8 12 | ) 13 | 14 | require ( 15 | fyne.io/systray v1.11.0 // indirect 16 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 17 | github.com/disintegration/imaging v1.6.3-0.20201218193011-d40f48ce0f09 // indirect 18 | github.com/dlclark/regexp2 v1.11.4 // indirect 19 | github.com/fogleman/gg v1.3.0 // indirect 20 | github.com/fredbi/uri v1.1.0 // indirect 21 | github.com/fsnotify/fsnotify v1.7.0 // indirect 22 | github.com/fyne-io/gl-js v0.1.0 // indirect 23 | github.com/fyne-io/glfw-js v0.2.0 // indirect 24 | github.com/fyne-io/image v0.1.1 // indirect 25 | github.com/fyne-io/oksvg v0.0.0-20250329173316-7ddb0d1149d1 // indirect 26 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect 27 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect 28 | github.com/go-text/render v0.2.0 // indirect 29 | github.com/go-text/typesetting v0.2.1 // indirect 30 | github.com/godbus/dbus/v5 v5.1.0 // indirect 31 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 32 | github.com/hack-pad/go-indexeddb v0.3.2 // indirect 33 | github.com/hack-pad/safejs v0.1.0 // indirect 34 | github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect 35 | github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect 36 | github.com/kr/text v0.2.0 // indirect 37 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 38 | github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect 39 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 40 | github.com/rymdport/portal v0.4.1 // indirect 41 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect 42 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect 43 | github.com/stretchr/testify v1.10.0 // indirect 44 | golang.org/x/image v0.24.0 // indirect 45 | golang.org/x/net v0.33.0 // indirect 46 | golang.org/x/sys v0.30.0 // indirect 47 | golang.org/x/text v0.22.0 // indirect 48 | gopkg.in/yaml.v3 v3.0.1 // indirect 49 | ) 50 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | fyne.io/fyne/v2 v2.6.0-rc1 h1:hAGKs/pzbmVCPQun93alc1wPtn0ZHWV4GHwwTRlJ0GI= 2 | fyne.io/fyne/v2 v2.6.0-rc1/go.mod h1:oHQ5Ptw72+3sGfesLJsB9ih0fjaqRel1GIpztbrlMJI= 3 | fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= 4 | fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= 5 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 6 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 7 | github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= 8 | github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 9 | github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= 10 | github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= 11 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 12 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 13 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 15 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/disintegration/imaging v1.6.3-0.20201218193011-d40f48ce0f09 h1:MJFqtdxTq94XqUgg7DcGCaOIXrDTJE/tPHK66Jshguc= 17 | github.com/disintegration/imaging v1.6.3-0.20201218193011-d40f48ce0f09/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 18 | github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= 19 | github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 20 | github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= 21 | github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= 22 | github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= 23 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 24 | github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= 25 | github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4= 26 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 27 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 28 | github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM= 29 | github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= 30 | github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM= 31 | github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= 32 | github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= 33 | github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= 34 | github.com/fyne-io/oksvg v0.0.0-20250329173316-7ddb0d1149d1 h1:3sX2TWEywNyd6M2Ym7ycXfO843W7xedwl6qfFdjyM+k= 35 | github.com/fyne-io/oksvg v0.0.0-20250329173316-7ddb0d1149d1/go.mod h1:nwtB20F3XhrmTxa77Hv9zG9gLJFTT4ppQz36ZCoKAj8= 36 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= 37 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= 38 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= 39 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 40 | github.com/go-pdf/fpdf v0.8.0 h1:IJKpdaagnWUeSkUFUjTcSzTppFxmv8ucGQyNPQWxYOQ= 41 | github.com/go-pdf/fpdf v0.8.0/go.mod h1:gfqhcNwXrsd3XYKte9a7vM3smvU/jB4ZRDrmWSxpfdc= 42 | github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= 43 | github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= 44 | github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= 45 | github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= 46 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= 47 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= 48 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 49 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 50 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 51 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 52 | github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= 53 | github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= 54 | github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= 55 | github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= 56 | github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= 57 | github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= 58 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 59 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 60 | github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc= 61 | github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= 62 | github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= 63 | github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= 64 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 65 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 66 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 67 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 68 | github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= 69 | github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= 70 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 71 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 72 | github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= 73 | github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= 74 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 75 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 76 | github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA= 77 | github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= 78 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= 79 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= 80 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= 81 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= 82 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 83 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 84 | github.com/watzon/goshot v0.7.1 h1:GguK8AyzlijKf2bctAfvTxKvjT/ECfsEzmgMfEAfzMw= 85 | github.com/watzon/goshot v0.7.1/go.mod h1:NncyO92VZ7znG3eZlvS4ziN9oZ3GBFVP0ktaxVQf53E= 86 | github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= 87 | github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 88 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 89 | golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= 90 | golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= 91 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 92 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 93 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 94 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 95 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 96 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 97 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 98 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 99 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 100 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 101 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 102 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | -------------------------------------------------------------------------------- /img/screenshot-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andydotxyz/slydes/74deb00b8a6c613269010d587f8a90147f4c6f93/img/screenshot-code.png -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andydotxyz/slydes/74deb00b8a6c613269010d587f8a90147f4c6f93/img/screenshot.png -------------------------------------------------------------------------------- /layout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | "fyne.io/fyne/v2/container" 6 | ) 7 | 8 | type aspectLayout struct { 9 | ratio float32 10 | } 11 | 12 | func (a *aspectLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) { 13 | width, height := size.Width, size.Height 14 | if width > height*a.ratio { 15 | width = height * a.ratio 16 | } else { 17 | height = width / a.ratio 18 | } 19 | 20 | inner := fyne.NewSize(width, height) 21 | pos := fyne.NewPos((size.Width-width)/2, (size.Height-height)/2) 22 | for _, o := range objs { 23 | o.Resize(inner) 24 | o.Move(pos) 25 | } 26 | } 27 | 28 | func (a *aspectLayout) MinSize([]fyne.CanvasObject) fyne.Size { 29 | return fyne.NewSize(80, 45) 30 | } 31 | 32 | func newAspectContainer(children ...fyne.CanvasObject) *fyne.Container { 33 | return container.New(&aspectLayout{ratio: 16.0 / 9.0}, children...) 34 | } 35 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | "fyne.io/fyne/v2" 10 | "fyne.io/fyne/v2/app" 11 | "fyne.io/fyne/v2/dialog" 12 | "fyne.io/fyne/v2/storage" 13 | ) 14 | 15 | func main() { 16 | a := app.New() 17 | w := a.NewWindow("Slydes") 18 | w.Resize(fyne.NewSize(600, 330)) 19 | 20 | s := newSlides() 21 | g := newGUI(s, w) 22 | w.SetContent(g.makeUI()) 23 | w.Canvas().Focus(g.content) 24 | 25 | flag.Parse() 26 | if len(flag.Args()) > 0 && len(flag.Args()[0]) > 0 { 27 | path := flag.Args()[0] 28 | 29 | f, _ := os.Open(path) 30 | data, err := ioutil.ReadAll(f) 31 | _ = f.Close() 32 | 33 | if err != nil { 34 | dialog.ShowError(err, g.win) 35 | } else { 36 | absPath, _ := filepath.Abs(path) 37 | g.s.uri = storage.NewFileURI(absPath) 38 | g.content.SetText(string(data)) 39 | } 40 | } 41 | 42 | w.ShowAndRun() 43 | } 44 | -------------------------------------------------------------------------------- /main.gui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | "strings" 6 | "time" 7 | 8 | "fyne.io/fyne/v2" 9 | "fyne.io/fyne/v2/canvas" 10 | "fyne.io/fyne/v2/container" 11 | "fyne.io/fyne/v2/data/binding" 12 | "fyne.io/fyne/v2/theme" 13 | "fyne.io/fyne/v2/widget" 14 | ) 15 | 16 | type gui struct { 17 | content *widget.Entry 18 | render *slide 19 | 20 | win fyne.Window 21 | s *slides 22 | 23 | refresh bool 24 | } 25 | 26 | func newGUI(s *slides, w fyne.Window) *gui { 27 | return &gui{s: s, win: w} 28 | } 29 | 30 | func (g *gui) makeUI() fyne.CanvasObject { 31 | g.content = widget.NewMultiLineEntry() 32 | border := canvas.NewRectangle(color.Transparent) 33 | border.StrokeColor = theme.PrimaryColor() 34 | border.StrokeWidth = 2 35 | border.CornerRadius = theme.InputRadiusSize() 36 | 37 | grid := container.NewGridWithRows(1) 38 | cellSize := fyne.NewSize(0, 0) 39 | refreshPreviews := func() { 40 | count, _ := g.s.count.Get() 41 | items := make([]fyne.CanvasObject, count+1) 42 | for i := 0; i < count; i++ { 43 | slide := g.newSlideButton(i) 44 | items[i] = container.NewPadded(slide) 45 | } 46 | items[count] = widget.NewButtonWithIcon("", theme.ContentAddIcon(), func() { 47 | rows := len(strings.Split(g.content.Text, "\n")) - 1 48 | 49 | g.content.CursorRow = rows + 2 50 | g.content.Append(` 51 | --- 52 | # New Slide 53 | `) 54 | }) 55 | grid.Objects = items 56 | cellSize = grid.Objects[0].MinSize() 57 | height := cellSize.Height - 4 58 | 59 | fyne.Do(func() { 60 | border.Resize(fyne.NewSize(height*16.0/9.0-3, height)) 61 | grid.Refresh() 62 | }) 63 | } 64 | 65 | var previewScroll *container.Scroll 66 | previews := container.NewStack(grid, container.NewWithoutLayout(border)) 67 | go refreshPreviews() 68 | moveHighlight := func(anim bool) { 69 | i, _ := g.s.current.Get() 70 | dest := fyne.NewPos(cellSize.Width*float32(i)+(theme.Padding()*float32(i-1))+6, 2) 71 | 72 | if previewScroll != nil { 73 | if dest.X < previewScroll.Offset.X { 74 | previewScroll.Offset.X = dest.X 75 | previewScroll.Refresh() 76 | } else if dest.X+border.Size().Width > previewScroll.Offset.X+previewScroll.Size().Width { 77 | previewScroll.Offset.X = dest.X + border.Size().Width - previewScroll.Size().Width 78 | previewScroll.Refresh() 79 | } 80 | } 81 | 82 | if !anim { 83 | border.Move(dest) 84 | return 85 | } 86 | 87 | canvas.NewPositionAnimation(border.Position(), dest, canvas.DurationShort, func(p fyne.Position) { 88 | border.Move(p) 89 | }).Start() 90 | } 91 | moveHighlight(false) 92 | g.s.current.AddListener(binding.NewDataListener(func() { 93 | fyne.Do(func() { 94 | moveHighlight(true) 95 | g.refreshSlide() 96 | }) 97 | })) 98 | 99 | g.render = newSlide("", g.s) 100 | g.content.OnChanged = func(s string) { 101 | g.refresh = true 102 | } 103 | g.content.OnCursorChanged = g.slideForCursor 104 | g.content.SetText("# Slide 1\n") 105 | 106 | split := container.NewHSplit(g.content, newAspectContainer(g.render)) 107 | split.Offset = 0.35 108 | play := &primaryAction{widget.NewToolbarAction(theme.MediaPlayIcon(), g.showPresentWindow)} 109 | 110 | go func() { 111 | for { 112 | time.Sleep(time.Second / 3) 113 | if !g.refresh { 114 | continue 115 | } 116 | g.refresh = false 117 | 118 | g.s.parseSource(g.content.Text) 119 | go refreshPreviews() 120 | g.slideForCursor() 121 | 122 | fyne.Do(func() { 123 | moveHighlight(true) 124 | g.refreshSlide() 125 | }) 126 | } 127 | }() 128 | 129 | previewScroll = container.NewHScroll(container.NewStack( 130 | canvas.NewRectangle(theme.MenuBackgroundColor()), 131 | container.NewHBox(previews))) 132 | 133 | return container.NewBorder( 134 | container.NewVBox( 135 | widget.NewToolbar( 136 | widget.NewToolbarAction(theme.FileIcon(), g.clearFile), 137 | widget.NewToolbarAction(theme.FolderOpenIcon(), g.openFile), 138 | widget.NewToolbarAction(theme.DocumentSaveIcon(), g.saveFile), 139 | widget.NewToolbarAction(theme.DocumentPrintIcon(), g.exportFile), 140 | widget.NewToolbarSeparator(), 141 | widget.NewToolbarAction(theme.NavigateBackIcon(), func() { 142 | i, _ := g.s.current.Get() 143 | if i > 0 { 144 | g.moveToSlide(i - 1) 145 | } 146 | }), 147 | widget.NewToolbarAction(theme.NavigateNextIcon(), func() { 148 | i, _ := g.s.current.Get() 149 | c, _ := g.s.count.Get() 150 | if i < c-1 { 151 | g.moveToSlide(i + 1) 152 | } 153 | }), 154 | play, 155 | widget.NewToolbarSpacer(), 156 | widget.NewToolbarAction(theme.HelpIcon(), func() {}), 157 | ), 158 | previewScroll), 159 | nil, 160 | nil, 161 | nil, 162 | split) 163 | } 164 | 165 | func (g *gui) moveToSlide(id int) { 166 | g.content.CursorColumn = 0 167 | if len(g.s.divideRows) == 0 || id == 0 { 168 | g.content.CursorRow = 0 169 | } else { 170 | div := g.s.divideRows[id-1] 171 | g.content.CursorRow = div + 1 172 | } 173 | g.content.Refresh() 174 | 175 | g.win.Canvas().Focus(g.content) 176 | _ = g.s.current.Set(id) 177 | } 178 | 179 | func (g *gui) slideForCursor() { 180 | id := 0 181 | for _, row := range g.s.divideRows { 182 | if g.content.CursorRow < row { 183 | break 184 | } else if g.content.CursorRow == row && g.content.CursorColumn < 3 { 185 | break // if it's a divide line, but not on the end 186 | } 187 | id++ 188 | } 189 | _ = g.s.current.Set(id) 190 | } 191 | 192 | func (g *gui) refreshSlide() { 193 | if g.render == nil { 194 | return 195 | } 196 | 197 | g.render.setSource(g.s.currentSource()) 198 | } 199 | 200 | type primaryAction struct { 201 | *widget.ToolbarAction 202 | } 203 | 204 | func (t *primaryAction) ToolbarObject() fyne.CanvasObject { 205 | button := t.ToolbarAction.ToolbarObject().(*widget.Button) 206 | button.Importance = widget.HighImportance 207 | 208 | return button 209 | } 210 | -------------------------------------------------------------------------------- /matrix.json: -------------------------------------------------------------------------------- 1 | { 2 | "Colors": { 3 | "background": "#111111ff", 4 | "foreground": "#999999ff", 5 | "bullet": "#999999ff", 6 | "header": "#003300ff", 7 | "subHeader": "#336699ff", 8 | "headerBackground": "#00ff0099" 9 | }, 10 | "Sizes": { 11 | "iconInline": 10.0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | "io" 6 | "log" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/alecthomas/chroma/v2/lexers" 11 | "github.com/watzon/goshot/pkg/chrome" 12 | "github.com/watzon/goshot/pkg/content/code" 13 | "github.com/watzon/goshot/pkg/render" 14 | "github.com/yuin/goldmark" 15 | "github.com/yuin/goldmark/ast" 16 | "github.com/yuin/goldmark/renderer" 17 | 18 | "fyne.io/fyne/v2" 19 | "fyne.io/fyne/v2/canvas" 20 | "fyne.io/fyne/v2/container" 21 | "fyne.io/fyne/v2/storage" 22 | ) 23 | 24 | type content struct { 25 | heading, subheading string 26 | bgpath string 27 | 28 | content []fyne.CanvasObject 29 | } 30 | 31 | func (s *slides) parseMarkdown(data string) content { 32 | c := content{} 33 | if data == "" { 34 | return c 35 | } 36 | 37 | r := &parser{c: &c, parent: s} 38 | md := goldmark.New(goldmark.WithRenderer(r)) 39 | err := md.Convert([]byte(data), nil) 40 | if err != nil { 41 | fyne.LogError("Failed to parse markdown", err) 42 | } 43 | return c 44 | } 45 | 46 | type parser struct { 47 | blockquote, heading, list, code bool 48 | parent *slides 49 | 50 | c *content 51 | } 52 | 53 | func (p *parser) AddOptions(...renderer.Option) {} 54 | 55 | func (p *parser) Render(_ io.Writer, source []byte, n ast.Node) error { 56 | tmpText := "" 57 | err := ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 58 | if !entering { 59 | switch n.Kind().String() { 60 | case "Heading": 61 | switch n.(*ast.Heading).Level { 62 | case 1: 63 | p.c.heading = tmpText 64 | case 2: 65 | p.c.subheading = tmpText 66 | default: 67 | log.Println("unsupported heading level", n.(*ast.Heading).Level) 68 | } 69 | case "Paragraph": 70 | // if p.blockquote // TODO 71 | if !p.list && tmpText != "" { 72 | p.c.content = append(p.c.content, canvas.NewText(tmpText+"\000", color.Black)) 73 | } 74 | case "ListItem": 75 | p.c.content = append(p.c.content, newBullet(tmpText, p.parent.theme)) 76 | case "CodeSpan": 77 | p.code = false 78 | } 79 | return ast.WalkContinue, p.handleExitNode(n) 80 | } 81 | 82 | switch n.Kind().String() { 83 | case "List": 84 | p.list = true 85 | case "ListItem": 86 | tmpText = "" 87 | case "Heading": 88 | p.heading = true 89 | tmpText = "" 90 | case "HorizontalRule", "ThematicBreak": // we won't get this as we're splitting slides 91 | case "Paragraph": 92 | tmpText = "" 93 | case "Text": 94 | if !p.code { 95 | ret := addTextToSegment(string(n.Text(source)), &tmpText, n) 96 | if ret != 0 { 97 | return ret, nil 98 | } 99 | } 100 | case "Blockquote": 101 | p.blockquote = true 102 | case "Image": 103 | name := string(n.(*ast.Image).Destination) 104 | path := filepath.Join(p.root(), name) 105 | if p.c.heading == "" { 106 | p.c.bgpath = path 107 | } else { 108 | img := canvas.NewImageFromFile(path) 109 | img.FillMode = canvas.ImageFillContain 110 | p.c.content = append(p.c.content, img) 111 | } 112 | case "CodeSpan": 113 | if !p.list && tmpText != "" { 114 | p.c.content = append(p.c.content, canvas.NewText(tmpText, color.Black)) 115 | } 116 | 117 | p.code = true 118 | inline := canvas.NewText(string(n.Text(source)), color.Black) 119 | bg := canvas.NewRectangle(color.Gray{Y: 0xcc}) 120 | p.c.content = append(p.c.content, container.NewStack(bg, inline)) 121 | tmpText = "" 122 | case "FencedCodeBlock", "CodeBlock": 123 | language := "" 124 | if c, ok := n.(*ast.FencedCodeBlock); ok { 125 | language = string(c.Language(source)) 126 | 127 | if language != "" { 128 | lex := lexers.Get(language) 129 | if lex == nil { 130 | log.Println("Failed to find lexer for language", language) 131 | language = "" 132 | } 133 | } 134 | } 135 | 136 | lines := n.Lines() 137 | raw := "" 138 | if lines.Len() > 0 { 139 | raw = string(source[lines.At(0).Start:lines.At(lines.Len()-1).Stop]) 140 | } 141 | 142 | codeContent := code.DefaultRenderer(raw). 143 | WithTheme("catppuccin-mocha"). // or "-latte" for light 144 | WithLanguage(language). 145 | WithLineNumbers(true). 146 | WithFontSize(42). 147 | WithMinWidth(600). 148 | WithMaxWidth(1900) 149 | 150 | draw := render.NewCanvas(). 151 | WithChrome(chrome.NewBlankChrome()). 152 | WithContent(codeContent) 153 | 154 | img, err := draw.RenderToImage() 155 | if err != nil { 156 | fyne.LogError("Failed to render code", err) 157 | } else { 158 | rendered := canvas.NewImageFromImage(img) 159 | rendered.FillMode = canvas.ImageFillContain 160 | p.c.content = append(p.c.content, rendered) 161 | } 162 | } 163 | 164 | return ast.WalkContinue, nil 165 | }) 166 | return err 167 | } 168 | 169 | func (p *parser) handleExitNode(n ast.Node) error { 170 | switch n.Kind().String() { 171 | case "Blockquote": 172 | p.blockquote = false 173 | case "List": 174 | p.list = false 175 | } 176 | return nil 177 | } 178 | 179 | func addTextToSegment(text string, s *string, node ast.Node) ast.WalkStatus { 180 | trimmed := strings.ReplaceAll(text, "\n", " ") // newline inside paragraph is not newline 181 | if trimmed == "" { 182 | return ast.WalkContinue 183 | } 184 | next := node.(*ast.Text).NextSibling() 185 | if next != nil { 186 | if nextText, ok := next.(*ast.Text); ok { 187 | if nextText.Segment.Start > node.(*ast.Text).Segment.Stop { // detect presence of a trailing newline 188 | trimmed = trimmed + " " 189 | } 190 | } 191 | } 192 | 193 | *s = *s + trimmed 194 | return 0 195 | } 196 | 197 | func (p *parser) root() string { 198 | if p.parent.uri == nil { 199 | return "" 200 | } 201 | 202 | dir, _ := storage.Parent(p.parent.uri) 203 | return dir.Path() 204 | } 205 | -------------------------------------------------------------------------------- /present.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | ) 6 | 7 | func (g *gui) showPresentWindow() { 8 | w2 := fyne.CurrentApp().NewWindow("Play") 9 | 10 | items := g.s.items 11 | id, _ := g.s.current.Get() 12 | content := newSlide(items[id], g.s) 13 | w2.SetPadded(false) 14 | w2.SetContent(newAspectContainer(content)) 15 | 16 | addPresentationKeys(w2, content, items, id) 17 | w2.SetFullScreen(true) 18 | w2.Show() 19 | } 20 | 21 | func addPresentationKeys(w fyne.Window, content *slide, items []string, id int) { 22 | w.Canvas().SetOnTypedKey(func(k *fyne.KeyEvent) { 23 | switch k.Name { 24 | case fyne.KeyEscape: 25 | w.Close() 26 | case fyne.KeyLeft, fyne.KeyUp: 27 | if id <= 0 { 28 | return 29 | } 30 | 31 | id-- 32 | content.setSource(items[id]) 33 | case fyne.KeyRight, fyne.KeyDown, fyne.KeySpace, fyne.KeyEnter, fyne.KeyReturn: 34 | if id >= len(items)-1 { 35 | return 36 | } 37 | 38 | id++ 39 | content.setSource(items[id]) 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /slide.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | "fyne.io/fyne/v2/canvas" 6 | "fyne.io/fyne/v2/container" 7 | "fyne.io/fyne/v2/theme" 8 | "fyne.io/fyne/v2/widget" 9 | ) 10 | 11 | type slideType int 12 | 13 | const ( 14 | otherSlide slideType = iota 15 | headingSlide 16 | imageSlide 17 | ) 18 | 19 | type slide struct { 20 | widget.BaseWidget 21 | variant slideType 22 | parent *slides 23 | 24 | content *fyne.Container 25 | bg fyne.CanvasObject 26 | heading, subheading *canvas.Text 27 | } 28 | 29 | func newSlide(data string, parent *slides) *slide { 30 | s := &slide{parent: parent} 31 | s.ExtendBaseWidget(s) 32 | 33 | s.bg = s.themeBackground() 34 | items := []fyne.CanvasObject{s.bg} 35 | s.heading = nil 36 | s.subheading = nil 37 | s.addContent(&items, parent.parseMarkdown(data)) 38 | s.content = container.NewWithoutLayout(items...) 39 | return s 40 | } 41 | 42 | func (s *slide) CreateRenderer() fyne.WidgetRenderer { 43 | return widget.NewSimpleRenderer(s.content) 44 | } 45 | 46 | func (s *slide) Resize(size fyne.Size) { 47 | s.bg.Resize(size) 48 | 49 | s.layout(size) 50 | s.BaseWidget.Resize(size) 51 | } 52 | 53 | func (s *slide) MinSize() fyne.Size { 54 | return fyne.NewSize(80, 45) // TODO de-duplicate 55 | } 56 | 57 | func (s *slide) addContent(items *[]fyne.CanvasObject, in content) { 58 | if in.bgpath != "" { 59 | img := canvas.NewImageFromFile(in.bgpath) 60 | img.ScaleMode = canvas.ImageScaleFastest 61 | *items = append(*items, img) 62 | s.variant = imageSlide 63 | return 64 | } 65 | 66 | if in.heading != "" { 67 | s.heading = canvas.NewText(in.heading, s.parent.theme.Color(colorNameHeader, theme.VariantLight)) 68 | s.heading.TextStyle.Bold = true 69 | s.variant = headingSlide 70 | *items = append(*items, s.heading) 71 | } 72 | if in.subheading != "" { 73 | s.subheading = canvas.NewText(in.subheading, s.parent.theme.Color(colorNameSubHeader, theme.VariantLight)) 74 | s.subheading.TextStyle.Bold = true 75 | 76 | s.variant = headingSlide 77 | *items = append(*items, s.subheading) 78 | } 79 | 80 | if len(in.content) > 0 { 81 | s.variant = otherSlide 82 | *items = append(*items, in.content...) 83 | } 84 | 85 | for _, o := range *items { 86 | if t, ok := o.(*canvas.Text); ok { 87 | if t == s.heading || t == s.subheading { 88 | continue 89 | } 90 | t.Color = s.parent.theme.Color(theme.ColorNameForeground, theme.VariantLight) 91 | } 92 | } 93 | } 94 | 95 | func (s *slide) setSource(data string) { 96 | s.bg = s.themeBackground() 97 | items := []fyne.CanvasObject{s.bg} 98 | s.heading = nil 99 | s.subheading = nil 100 | s.addContent(&items, s.parent.parseMarkdown(data)) 101 | s.content.Objects = items 102 | s.content.Refresh() 103 | s.Resize(s.Size()) 104 | } 105 | -------------------------------------------------------------------------------- /slidelayout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | "fyne.io/fyne/v2/theme" 6 | ) 7 | 8 | const slideHeight = float32(240) 9 | 10 | func (s *slide) layout(size fyne.Size) { 11 | scale := size.Height / slideHeight 12 | 13 | switch s.variant { 14 | case headingSlide: 15 | s.layoutTitleSlide(size, scale) 16 | case imageSlide: 17 | s.layoutImage(size) 18 | default: 19 | s.layoutFallback(size, scale) 20 | } 21 | } 22 | 23 | func (s *slide) layoutTitleSlide(size fyne.Size, scale float32) { 24 | height := float32(0) 25 | if s.heading != nil { 26 | s.heading.TextSize = theme.TextHeadingSize() * scale 27 | s.heading.Alignment = fyne.TextAlignCenter 28 | 29 | headHeight := s.heading.MinSize().Height 30 | height = headHeight 31 | s.heading.Resize(fyne.NewSize(size.Width, headHeight)) 32 | s.heading.Refresh() 33 | } 34 | if s.subheading != nil { 35 | s.subheading.TextSize = theme.TextSubHeadingSize() * scale 36 | s.subheading.Alignment = fyne.TextAlignCenter 37 | 38 | subHeight := s.subheading.MinSize().Height 39 | height += subHeight 40 | s.subheading.Resize(fyne.NewSize(size.Width, subHeight)) 41 | s.subheading.Refresh() 42 | } 43 | y := (size.Height - height) / 2 44 | if s.heading != nil { 45 | s.heading.Move(fyne.NewPos(0, y)) 46 | } 47 | if s.subheading != nil { 48 | subHeight := s.subheading.MinSize().Height 49 | s.subheading.Move(fyne.NewPos(0, y+height-subHeight)) 50 | } 51 | } 52 | 53 | func (s *slide) layoutFallback(size fyne.Size, scale float32) { 54 | skip := 1 55 | pad := theme.Padding() * scale 56 | y := pad 57 | if s.heading != nil { 58 | skip++ 59 | s.heading.TextSize = theme.TextHeadingSize() * scale 60 | s.heading.Move(fyne.NewPos(pad, pad)) 61 | s.heading.Refresh() 62 | y += s.heading.MinSize().Height //+ pad 63 | } 64 | subPad := float32(0) 65 | if s.subheading != nil { 66 | skip++ 67 | s.subheading.TextSize = theme.TextSubHeadingSize() * scale 68 | s.subheading.Move(fyne.NewPos(pad, y)) 69 | s.subheading.Refresh() 70 | subPad = s.subheading.MinSize().Height 71 | } 72 | 73 | contentSize := size.SubtractWidthHeight(pad*2, size.Height/9*2+subPad+pad*2) 74 | contentPos := fyne.NewPos(pad, size.Height/6+subPad+pad) 75 | layoutContent(s.content.Objects[skip:], scale, contentSize, contentPos) 76 | } 77 | 78 | func (s *slide) layoutImage(size fyne.Size) { 79 | for _, o := range s.content.Objects[1:] { 80 | o.Resize(size) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /slides.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "fyne.io/fyne/v2" 10 | "fyne.io/fyne/v2/data/binding" 11 | "fyne.io/fyne/v2/theme" 12 | 13 | "github.com/BurntSushi/toml" 14 | ) 15 | 16 | type slides struct { 17 | count, current binding.Int 18 | uri fyne.URI 19 | theme fyne.Theme 20 | 21 | divideRows []int 22 | items []string 23 | config config 24 | } 25 | 26 | func newSlides() *slides { 27 | s := &slides{count: binding.NewInt(), current: binding.NewInt(), 28 | divideRows: make([]int, 0), items: []string{""}, 29 | theme: &slideTheme{Theme: theme.DefaultTheme()}} 30 | _ = s.count.Set(1) 31 | return s 32 | } 33 | 34 | func (s *slides) parseSource(in string) { 35 | id := 0 36 | data := "" 37 | items := make([]string, 0) 38 | breaks := make([]int, 0) 39 | s.config = config{} 40 | 41 | scanner := bufio.NewScanner(strings.NewReader(in)) 42 | row := 0 43 | frontMatter := false 44 | header := "" 45 | for scanner.Scan() { 46 | line := scanner.Text() 47 | trim := strings.TrimSpace(line) 48 | if trim == "+++" { 49 | row++ 50 | if frontMatter { 51 | frontMatter = false 52 | s.config = s.parseHeader(strings.TrimSpace(header)) 53 | continue 54 | } else { 55 | frontMatter = true 56 | continue 57 | } 58 | } 59 | if frontMatter { 60 | row++ 61 | header += trim + "\n" 62 | continue 63 | } 64 | if strings.TrimSpace(line) == "---" { 65 | items = append(items, data) 66 | breaks = append(breaks, row) 67 | id++ 68 | data = "" 69 | } else { 70 | data = data + "\n" + line 71 | } 72 | row++ 73 | } 74 | items = append(items, data) 75 | 76 | _ = s.count.Set(len(items)) 77 | id, _ = s.current.Get() 78 | if id >= len(items) { 79 | _ = s.current.Set(id) 80 | } 81 | s.items = items 82 | s.divideRows = breaks 83 | } 84 | 85 | func (s *slides) currentSource() string { 86 | id, _ := s.current.Get() 87 | return s.items[id] 88 | } 89 | 90 | func (s *slides) parseHeader(blob string) (c config) { 91 | _, err := toml.Decode(blob, &c) 92 | if err != nil { // don't print as it will likely be partial content 93 | return c 94 | } 95 | s.theme = &slideTheme{Theme: theme.DefaultTheme()} 96 | 97 | if c.Theme != "" { 98 | path := filepath.Join(filepath.Dir(s.uri.Path()), c.Theme+".json") 99 | 100 | f, err := os.Open(path) 101 | if err == nil { 102 | th, err := theme.FromJSONReader(f) 103 | f.Close() 104 | if err == nil { 105 | s.theme = th 106 | } 107 | } 108 | } 109 | return c 110 | } 111 | -------------------------------------------------------------------------------- /slidetheme.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | "fyne.io/fyne/v2/canvas" 6 | "fyne.io/fyne/v2/container" 7 | "fyne.io/fyne/v2/theme" 8 | ) 9 | 10 | func (s *slide) themeBackground() fyne.CanvasObject { 11 | bg := canvas.NewRectangle(s.parent.theme.Color(theme.ColorNameBackground, theme.VariantLight)) 12 | top := canvas.NewRectangle(s.parent.theme.Color(colorNameHeaderBackground, theme.VariantLight)) 13 | bottom := canvas.NewRectangle(s.parent.theme.Color(colorNameHeaderBackground, theme.VariantLight)) 14 | return container.New(&backgroundLayout{s: s}, bg, top, bottom) 15 | } 16 | 17 | type backgroundLayout struct { 18 | s *slide 19 | } 20 | 21 | func (l *backgroundLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) { 22 | objs[0].Resize(size) 23 | 24 | top := objs[1] 25 | bottom := objs[2] 26 | if l.s.variant == headingSlide { 27 | top.Resize(fyne.NewSize(size.Width, size.Height/4)) 28 | top.Move(fyne.NewPos(0, size.Height*3/8)) 29 | 30 | bottom.Hide() 31 | return 32 | } 33 | 34 | top.Resize(fyne.NewSize(size.Width, size.Height/6)) 35 | top.Move(fyne.Position{}) 36 | 37 | bottomHeight := size.Height / 18 38 | bottom.Show() 39 | bottom.Resize(fyne.NewSize(size.Width, bottomHeight)) 40 | bottom.Move(fyne.NewPos(0, size.Height-bottomHeight)) 41 | } 42 | 43 | func (l *backgroundLayout) MinSize([]fyne.CanvasObject) fyne.Size { 44 | return fyne.Size{} 45 | } 46 | -------------------------------------------------------------------------------- /slideutil.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | "fyne.io/fyne/v2/canvas" 6 | "fyne.io/fyne/v2/theme" 7 | ) 8 | 9 | func layoutContent(objs []fyne.CanvasObject, scale float32, size fyne.Size, pos fyne.Position) { 10 | splitAt := -1 11 | for i, o := range objs { 12 | if _, ok := o.(*canvas.Image); ok { 13 | splitAt = i 14 | } 15 | } 16 | 17 | pad := theme.Padding() * scale 18 | width := size.Width 19 | if splitAt > -1 && len(objs) > 1 { 20 | width = (width - pad) / 2 21 | } 22 | x := pos.X 23 | y := pos.Y 24 | if splitAt == 0 { 25 | x = x + width + pad 26 | } 27 | 28 | leftEdge := x 29 | inline := false 30 | for i, o := range objs { 31 | switch t := o.(type) { 32 | case *canvas.Text: 33 | t.TextSize = theme.TextSize() * scale 34 | 35 | if len(t.Text) > 0 && t.Text[len(t.Text)-1] != '\000' { 36 | inline = true 37 | } 38 | case slideWidget: 39 | t.setScale(scale) 40 | case *fyne.Container: 41 | if len(t.Objects) == 2 { 42 | if t, ok := t.Objects[1].(*canvas.Text); ok { 43 | t.TextSize = theme.TextSize() * scale 44 | inline = true 45 | } 46 | } 47 | } 48 | 49 | if splitAt == i { 50 | o.Resize(fyne.NewSize(width, size.Height)) 51 | if splitAt == 0 { 52 | o.Move(fyne.NewPos(pos.X, pos.Y)) 53 | } else { 54 | o.Move(fyne.NewPos(x+width+pad, pos.Y)) 55 | } 56 | } else { 57 | o.Move(fyne.NewPos(x, y)) 58 | if inline { 59 | o.Resize(o.MinSize()) 60 | x += o.MinSize().Width 61 | 62 | inline = false 63 | } else { 64 | o.Resize(fyne.NewSize(width, o.MinSize().Height)) 65 | x = leftEdge 66 | y += o.MinSize().Height + pad 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /slidewidgets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | "fyne.io/fyne/v2/canvas" 6 | "fyne.io/fyne/v2/container" 7 | "fyne.io/fyne/v2/test" 8 | "fyne.io/fyne/v2/theme" 9 | "fyne.io/fyne/v2/widget" 10 | ) 11 | 12 | type slideWidget interface { 13 | setScale(float32) 14 | } 15 | 16 | type bullet struct { 17 | widget.BaseWidget 18 | theme fyne.Theme 19 | 20 | content string 21 | scale float32 22 | 23 | dot *canvas.Circle 24 | text *canvas.Text 25 | } 26 | 27 | func newBullet(txt string, th fyne.Theme) *bullet { 28 | return &bullet{theme: th, content: txt, scale: 1} 29 | } 30 | 31 | func (b *bullet) CreateRenderer() fyne.WidgetRenderer { 32 | b.dot = canvas.NewCircle(b.theme.Color(colorNameBullet, theme.VariantLight)) 33 | b.text = canvas.NewText(b.content, b.theme.Color(colorNameBullet, theme.VariantLight)) 34 | return widget.NewSimpleRenderer(container.NewWithoutLayout(b.dot, b.text)) 35 | } 36 | 37 | func (b *bullet) Refresh() { 38 | if b.dot != nil { 39 | b.dot.FillColor = b.theme.Color(colorNameBullet, theme.VariantLight) 40 | b.dot.Refresh() 41 | } 42 | if b.text != nil { 43 | b.text.Color = b.theme.Color(colorNameBullet, theme.VariantLight) 44 | b.text.Refresh() 45 | } 46 | } 47 | 48 | func (b *bullet) Resize(size fyne.Size) { 49 | b.dot.Move(fyne.NewPos(0, (size.Height-b.dot.Size().Height)/2)) 50 | b.text.Move(fyne.NewPos(b.dot.Size().Width+theme.Padding()*b.scale, 0)) 51 | b.text.Resize(fyne.NewSize(size.Width-b.dot.Size().Width-theme.Padding()*b.scale, size.Height)) 52 | } 53 | 54 | func (b *bullet) MinSize() fyne.Size { 55 | if b.text == nil { 56 | return fyne.NewSize(14, 4) 57 | } 58 | 59 | return b.dot.Size().Add(b.text.MinSize()).AddWidthHeight(theme.Padding()*b.scale, 0) 60 | } 61 | 62 | func (b *bullet) setScale(scale float32) { 63 | _ = test.WidgetRenderer(b) 64 | b.scale = scale 65 | 66 | b.dot.Resize(fyne.NewSize(5*scale, 5*scale)) 67 | b.text.TextSize = theme.TextSize() * scale 68 | } 69 | -------------------------------------------------------------------------------- /style.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | 6 | "fyne.io/fyne/v2" 7 | "fyne.io/fyne/v2/theme" 8 | ) 9 | 10 | const ( 11 | colorNameBullet = "bullet" 12 | colorNameHeader = "header" 13 | colorNameSubHeader = "subHeader" 14 | colorNameHeaderBackground = "headerBackground" 15 | ) 16 | 17 | type slideTheme struct { 18 | fyne.Theme 19 | } 20 | 21 | func (s *slideTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { 22 | switch n { 23 | case theme.ColorNameForeground, colorNameBullet: 24 | return color.Black 25 | case theme.ColorNameBackground: 26 | return color.White 27 | case colorNameHeader: 28 | return color.Gray{Y: 0x20} 29 | case colorNameSubHeader: 30 | return color.Gray{Y: 0x50} 31 | case colorNameHeaderBackground: 32 | return color.Gray{Y: 0xC0} 33 | default: 34 | return s.Theme.Color(n, v) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /testdata/export.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andydotxyz/slydes/74deb00b8a6c613269010d587f8a90147f4c6f93/testdata/export.pdf -------------------------------------------------------------------------------- /widgets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "fyne.io/fyne/v2" 7 | "fyne.io/fyne/v2/canvas" 8 | "fyne.io/fyne/v2/container" 9 | "fyne.io/fyne/v2/theme" 10 | "fyne.io/fyne/v2/widget" 11 | ) 12 | 13 | type slideButton struct { 14 | widget.BaseWidget 15 | id int 16 | content fyne.CanvasObject 17 | g *gui 18 | } 19 | 20 | func (s *slideButton) CreateRenderer() fyne.WidgetRenderer { 21 | num := fmt.Sprintf(" %d", s.id+1) 22 | return widget.NewSimpleRenderer(container.NewStack(s.content, container.NewVBox(canvas.NewText(num, theme.BackgroundColor())))) 23 | } 24 | 25 | func (s *slideButton) Tapped(_ *fyne.PointEvent) { 26 | s.g.moveToSlide(s.id) 27 | } 28 | 29 | func (g *gui) newSlideButton(id int) fyne.CanvasObject { 30 | slide := newAspectContainer(newSlide(g.s.items[id], g.s)) 31 | button := &slideButton{id: id, content: slide, g: g} 32 | button.ExtendBaseWidget(button) 33 | return button 34 | } 35 | --------------------------------------------------------------------------------