├── LICENSE ├── README.md ├── changemonitor.go ├── cmd ├── cmdgui │ ├── cmdgui.go │ ├── curl.sh │ ├── plot │ ├── plot.sh │ └── test.sh └── demo │ └── main.go ├── driver ├── driver.go ├── driver_gio.go ├── driver_gotk3.go ├── driver_gtk2.go ├── driver_shiny.go ├── gio │ └── view.go ├── gotk3 │ └── view.go ├── gtk2 │ └── view.go └── shiny │ └── view.go ├── examples ├── animgif │ └── animgif.go ├── lsystem │ ├── demo.go │ ├── lsystem.go │ └── lsystemrender.go ├── mandelbrot │ ├── LICENSE_skeys │ ├── README.md │ ├── mandelbrot.go │ └── mandelmodel.go └── maze │ ├── maze.go │ └── mazegui.go ├── go.mod ├── model └── tile │ ├── README.md │ ├── tile.go │ ├── tilecache.go │ ├── tileprovider.go │ └── tilerendermodel.go ├── rendermodel.go ├── renderparameter.go └── renderview.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Howard C. Shaw III 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RenderView # 2 | ================ 3 | 4 | [![GoDoc](https://godoc.org/github.com/TheGrum/renderview?status.svg)](https://godoc.org/github.com/TheGrum/renderview) 5 | 6 | Install: 7 | ``` 8 | go get github.com/TheGrum/renderview 9 | ``` 10 | 11 | Needs either Shiny (limited functionality), Gio, go-gtk, or gotk3. The latter two require the corresponding GTK library installed. 12 | 13 | ===== 14 | 15 | A tool to quickly and easily wrap algorithms, image generation functions, web services, and the like with an interactive GUI interface offering panning, zooming, paging, and editable parameters. 16 | 17 | This is *not* meant as a replacement for a general GUI toolkit. It is meant for programmers who lack the time to invest in learning yet another graphical toolkit and all of its quirks and foibles just to throw up a quick interactive interface to get a feel for the behavior of back-end code. 18 | 19 | [![YouTube demo of using renderview to create a Mandelbrot viewer](http://img.youtube.com/vi/vG05T5LE9ZY/0.jpg)](http://www.youtube.com/watch?v=vG05T5LE9ZY "RenderView for Go Mandelbrot demo") 20 | 21 | ## Model-View-Controller 22 | 23 | Basically, RenderView is a View+Controller combination that operates on a Model you write - except that for many tasks, the built-in models will suffice, and all you have to implement is an image generation function. (Technically, I suppose, it is actually splitting the View into two parts, one that you write for the image generation, and one that RenderView sets up for editing parameters.) 24 | 25 | ### RenderView 26 | 27 | The eponymous control, this comes in multiple flavors, and handles window creation, view rendering, and control/event-handling. 28 | 29 | #### Shiny 30 | 31 | Shiny is an experimental native cross-platform GUI package for Go. At the moment it is usable only as a framebuffer+event loop. If all you need is the output of your image generation function with panning and zooming, RenderView+Shiny supports that. 32 | 33 | Build with -tags "shiny nogtk2" to use the Shiny backend. 34 | 35 | #### Gio 36 | 37 | Gio is an immediate-mode GUI package for Go. RenderView on the Gio backend supports 38 | automatic parameter editing widget generation in addition to the interactive image. 39 | 40 | Build with -tags "gio nogtk2" to use the Gio backend. 41 | 42 | #### go-gtk 43 | 44 | go-gtk is a functional CGo based GTK2 binding. RenderView on the go-gtk backend supports automatic parameter editing widget generation in addition to the interactive image. 45 | 46 | This is currently the default backend, so no -tags line is required. 47 | 48 | #### gotk3 49 | 50 | gotk3 is a functional CGo based GTK3 binding. RenderView on the gotk3 backend supports automatic parameter editing widget generation in addition to the interactive image. 51 | 52 | Build with -tags "gotk3 nogtk2" to use the gotk3 backend. 53 | 54 | ### RenderParameter 55 | 56 | Each RenderParameter carries a type string, allowing the renderView code to read and set the values without reflection. There is also a blank parameter that is automatically returned when a missing parameter is requested, allowing the RenderView code to behave as if the parameters it uses are always present. By either including or omitting the default parameters, you can control whether your code pays attention to certain controller behaviors. 57 | 58 | Hints can be provided to indicate whether parameters are only for use in communicating with the View and Controller, or should be exposed to the user. 59 | 60 | ZoomRenderParameter in the Mandelbrot example provides a demonstration of using a custom parameter to react immediately to changes in a value and, by setting other parameters in response, implement custom behavior. 61 | 62 | ### ChangeMonitor 63 | 64 | A means to observe a subset of RenderParameters and determine if they have changed since the value was last checked. 65 | 66 | ## RenderModel 67 | 68 | You can implement this to customize what information gets collected from the GUI and passed to your visualization code, and what gets returned. 69 | 70 | Basically, this boils down to a bag of parameters and a Render function that the view calls. 71 | 72 | #### BasicRenderModel 73 | 74 | In most cases, the BasicRenderModel will suffice. It provides a concrete implementation of the RenderModel interface and adds a throttling mechanism that ensures that requests for a new rendering from the view code only get passed through to your code when you do not already have a render in process, provided that you signal it by setting the Rendering bool appropriately at the start and end of your render. 75 | 76 | It moves rendering to a separate Goroutine, preventing a long-running image generator from freezing the UI. 77 | 78 | #### TileRenderModel 79 | 80 | The TileRenderModel implements a RenderModel that operates on map tiles such as those used in the OpenStreetMaps project. Look in models/tile for a TileRenderModel specific README.md. 81 | 82 | # Usage 83 | 84 | At its most basic, using RenderView with the BasicRenderModel can be as simple as adding a few lines of code: 85 | 86 | ``` 87 | m := rv.NewBasicRenderModel() 88 | m.AddParameters(DefaultParameters(false, rv.HINT_SIDEBAR, rv.OPT_AUTO_ZOOM, -10, 10, 10, -10)...) 89 | m.InnerRender = func() { 90 | // some number of m.Param[x].Value[Float64|Int|etc]() to gather the values your renderer needs 91 | m.Img = your_rendering_function_here(param, param, param) 92 | } 93 | driver.Main(m) 94 | ``` 95 | 96 | Alternately, you can fully specify your parameters, like so: 97 | 98 | ``` 99 | m.AddParameters( 100 | rv.SetHints(rv.HINT_HIDE, 101 | rv.NewIntRP("width", 0), 102 | rv.NewIntRP("height", 0), 103 | )...) 104 | m.AddParameters( 105 | rv.SetHints(rv.HINT_SIDEBAR, 106 | rv.NewIntRP("page", 0), 107 | rv.NewIntRP("linewidth", 1), 108 | rv.NewIntRP("cellwidth", 5), 109 | rv.NewIntRP("mazewidth", 100), 110 | rv.NewIntRP("mazeheight", 100))...) 111 | ``` 112 | 113 | #### Useful parameters 114 | 115 | You can have as many parameters as you like, but certain paramaters if present have special meaning to the views. 116 | 117 | * left,top,right,bottom - these can be either int or float64, and when available, operate panning, and if float64, zooming. - two way, you can change these in your code to move the viewport if you are paying attention to them 118 | * width,height - these get populated with the window width and height - changing these in your code has no effect. 119 | * options - maybe more later, right now these just control the zooming (done with the scroll-wheel) 120 | const ( 121 | OPT_NONE = iota // 0 122 | OPT_CENTER_ZOOM = 1 << iota // 1 123 | OPT_AUTO_ZOOM = 1 << iota // 2 124 | ) 125 | * zoom - int or float64, this gets incremented/decremented when the scroll-wheel is turned, and can be used to implement your own zoom. 126 | * mouseX, mouseY - float64, these get populated with the current mouse position in the window 127 | * page - this gets incremented/decremented by PgUp and PgDown when the graphical window has the focus, allowing for a paged environment. You can manipulated these from a custom zoom parameter to tie scrolling to paging if desired. 128 | 129 | See examples and cmd for more. 130 | 131 | Some examples require -tags "example" to build. 132 | 133 | ### cmdgui 134 | 135 | More than an example, this is a tool that applies the automatic GUI creation concept to command line applications or, through the medium of curl and other url-grabbing applications, to web services. It uses Go Templates to perform argument rewriting, and exports all parameters to the environment as well. 136 | 137 | ``` 138 | #!/bin/sh 139 | ./cmdgui -extraflags="func,string,sin(x);x" "./plot" "{{$.func}} {{$.left}} {{$.right}} {{$.bottom}} {{$.top}}" 140 | ``` 141 | 142 | This one line example takes a python command line plot generator, and turns it into an interactive function plotter supporting changing the function, panning, zooming, and hand-entering of plot axis dimensions. 143 | 144 | # Screenshots 145 | 146 | ![Mandelbrot](http://i.imgur.com/11H40dZ.png) 147 | ![cmdgui plot](http://i.imgur.com/VQSrwRv.png) 148 | ![maze](http://i.imgur.com/XG75kpZ.png) 149 | ![maze2](http://i.imgur.com/qCrmmUe.png) 150 | ![lsystem](http://i.imgur.com/kOvCrCR.png) 151 | ![map](http://i.imgur.com/MIwJRa5.png) 152 | 153 | -------------------------------------------------------------------------------- /changemonitor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package renderview 6 | 7 | import "sync" 8 | 9 | // ChangeMonitor implements comparing the value 10 | // of a subset of parameters to determine whether 11 | // a redraw or recalculation is required 12 | type ChangeMonitor struct { 13 | lastValue string 14 | 15 | sync.Mutex 16 | Params []RenderParameter 17 | } 18 | 19 | func NewChangeMonitor() *ChangeMonitor { 20 | return &ChangeMonitor{ 21 | lastValue: "", 22 | Params: make([]RenderParameter, 0, 10), 23 | } 24 | } 25 | 26 | func (c *ChangeMonitor) AddParameters(params ...RenderParameter) { 27 | c.Lock() 28 | defer c.Unlock() 29 | 30 | c.Params = append(c.Params, params...) 31 | } 32 | 33 | func (c *ChangeMonitor) HasChanged() bool { 34 | c.Lock() 35 | defer c.Unlock() 36 | 37 | newValue := GetParameterStatusString(c.Params...) 38 | //fmt.Printf("Comparing (%v) to (%v)\n", c.lastValue, newValue) 39 | if c.lastValue != newValue { 40 | c.lastValue = newValue 41 | return true 42 | } 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /cmd/cmdgui/cmdgui.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "flag" 10 | "html/template" 11 | "image" 12 | _ "image/gif" 13 | _ "image/jpeg" 14 | "image/png" 15 | "log" 16 | "os" 17 | "os/exec" 18 | "strconv" 19 | "strings" 20 | 21 | "github.com/TheGrum/renderview/driver" 22 | 23 | rv "github.com/TheGrum/renderview" 24 | ) 25 | 26 | var ( 27 | defaultFlags = flag.Bool("defaultflags", true, "include default flags (left, top, right, bottom, width, height, options)") 28 | extraFlags = flag.String("extraflags", "", "quoted comma-separated list, name, type, and starting value, e.g. \"left,float64,0,top,float64,0\"") 29 | watchFile = flag.String("watch", "", "if defined, image file to read in after executing command") 30 | ) 31 | 32 | func main() { 33 | flag.Parse() 34 | 35 | cmd := flag.Arg(0) 36 | args := flag.Arg(1) 37 | argTemplate := template.Must(template.New("arg").Parse(args)) 38 | 39 | m := rv.NewBasicRenderModel() 40 | if *defaultFlags { 41 | m.AddParameters( 42 | rv.SetHints(rv.HINT_SIDEBAR, 43 | rv.NewFloat64RP("left", 0), 44 | rv.NewFloat64RP("top", 0), 45 | rv.NewFloat64RP("right", 100), 46 | rv.NewFloat64RP("bottom", 100), 47 | rv.NewIntRP("width", 100), 48 | rv.NewIntRP("height", 100), 49 | rv.NewIntRP("options", rv.OPT_AUTO_ZOOM))...) 50 | } 51 | m.AddParameters(rv.SetHints(rv.HINT_SIDEBAR, createExtraFlags(*extraFlags)...)...) 52 | m.InnerRender = getInnerRender(m, cmd, argTemplate) 53 | m.InnerRender() 54 | driver.Main(m) 55 | } 56 | 57 | func getInnerRender(m *rv.BasicRenderModel, cmd string, argtemplate *template.Template) func() { 58 | return func() { 59 | InnerRender(m, cmd, argtemplate) 60 | } 61 | } 62 | 63 | func InnerRender(m *rv.BasicRenderModel, cmd string, argtemplate *template.Template) { 64 | flags := m.GetParameterNames() 65 | templateMap := make(map[string]string) 66 | for _, k := range flags { 67 | p := m.GetParameter(k) 68 | v := rv.GetParameterValueAsString(p) 69 | templateMap[k] = v 70 | os.Setenv(k, v) 71 | } 72 | b := make([]byte, 0, 8192) 73 | argresult := bytes.NewBuffer(b) 74 | //fmt.Printf("templateMap: %v\n", templateMap) 75 | err := argtemplate.Execute(argresult, templateMap) 76 | handleError(err) 77 | args := parseArgs(argresult.String()) 78 | //fmt.Printf("args: %v\n", args) 79 | 80 | wf := *watchFile 81 | command := exec.Command(cmd, args...) 82 | //fmt.Printf("command: %v\nwf: %v\nwf == \"\": %v\n", command, wf, wf == "") 83 | if wf == "" { 84 | o, err := command.Output() 85 | obuf := bytes.NewReader(o) 86 | img, _, err := image.Decode(obuf) 87 | //handleError(err) 88 | if err != nil { 89 | obuf = bytes.NewReader(o) 90 | img, err = png.Decode(obuf) 91 | } 92 | //handleError(err) 93 | //fmt.Println("img is a type of:", reflect.TypeOf(img)) 94 | if err != nil { 95 | return 96 | } 97 | m.Img = img 98 | } else { 99 | command.Run() 100 | f, err := os.Open(wf) 101 | if err != nil { 102 | return 103 | } 104 | //handleError(err) 105 | img, _, err := image.Decode(f) 106 | //handleError(err) 107 | f.Close() 108 | if err != nil { 109 | return 110 | } 111 | m.Img = img 112 | } 113 | } 114 | 115 | func parseArgs(argstring string) []string { 116 | // fmt.Print(argstring) 117 | args := make([]string, 0, 10) 118 | b := make([]byte, 0, len(argstring)) 119 | buf := bytes.NewBuffer(b) 120 | curquote := "" 121 | backslash := "" 122 | for _, s := range argstring { 123 | if s == ' ' && curquote == "" && backslash == "" { 124 | args = append(args, buf.String()) 125 | //fmt.Printf("%v\n", args) 126 | buf.Reset() 127 | } else { 128 | buf.WriteRune(s) 129 | } 130 | if backslash == "" && curquote == "" { 131 | if s == '\'' || s == '"' || s == '`' { 132 | curquote = string(s) 133 | } 134 | } else if curquote != "" && curquote == string(s) && backslash == "" { 135 | curquote = "" 136 | } 137 | if s == '\\' && backslash == "" { 138 | backslash = "\\" 139 | } else { 140 | backslash = "" 141 | } 142 | // fmt.Printf("s:%v curquote:%v backslash:%v\n", string(s), curquote, backslash) 143 | 144 | } 145 | if buf.Len() > 0 { 146 | args = append(args, buf.String()) 147 | 148 | } 149 | return args 150 | 151 | } 152 | 153 | func createExtraFlags(extraflags string) []rv.RenderParameter { 154 | // fmt.Printf("createExtraFlags: %v", extraflags) 155 | split := strings.Split(extraflags, ",") 156 | // fmt.Print(split) 157 | if len(split) < 3 { 158 | flags := make([]rv.RenderParameter, 0, 0) 159 | return flags 160 | } 161 | flags := make([]rv.RenderParameter, 0, len(split)/3) 162 | for i := 0; i < (len(split) / 3); i++ { 163 | switch split[i*3+1] { 164 | case "int": 165 | iv, err := strconv.Atoi(split[i*3+2]) 166 | handleError(err) 167 | flag := rv.NewIntRP(split[i*3], iv) 168 | flags = append(flags, flag) 169 | case "uint32": 170 | iv, err := strconv.ParseInt(split[i*3+2], 10, 32) 171 | handleError(err) 172 | flag := rv.NewUInt32RP(split[i*3], uint32(iv)) 173 | flags = append(flags, flag) 174 | case "float64": 175 | fv, err := strconv.ParseFloat(split[i*3+2], 64) 176 | handleError(err) 177 | flag := rv.NewFloat64RP(split[i*3], fv) 178 | flags = append(flags, flag) 179 | case "complex128": 180 | cv, err := rv.ParseComplex(split[i*3+2]) 181 | handleError(err) 182 | flag := rv.NewComplex128RP(split[i*3], cv) 183 | flags = append(flags, flag) 184 | default: 185 | flag := rv.NewStringRP(split[i*3], split[i*3+2]) 186 | flags = append(flags, flag) 187 | 188 | } 189 | } 190 | //fmt.Print(flags) 191 | return flags 192 | 193 | } 194 | 195 | func handleError(err error) { 196 | if err != nil { 197 | log.Fatal(err) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /cmd/cmdgui/curl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./cmdgui -defaultflags=false -extraflags="width,int,0,height,int,0,page,int,0,search,string,puppy" "curl" "http://loremflickr.com/{{$.width}}/{{$.height}}/{{$.search}}" 4 | 5 | -------------------------------------------------------------------------------- /cmd/cmdgui/plot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | #https://github.com/drdrang/simple-plotting 4 | 5 | from __future__ import division 6 | from numpy import * 7 | import matplotlib.pyplot as plt 8 | import sys 9 | import getopt 10 | 11 | usage = '''plot [option] 'functions' minx maxx [miny maxy] 12 | 13 | Options: 14 | -h, --help print this message 15 | -p, --pdf generate PDF plot 16 | 17 | Generate a PNG (default) or PDF plot of the given function(s). The 18 | independent variable in the function(s) is x. Multiple functions are 19 | separated by a semicolon. The min and max values can be given as 20 | expressions. The output is typically redirected to a file. 21 | 22 | Example: 23 | plot 'tan(x); x' pi 3*pi/2-.01 0 5 > plot.png 24 | 25 | ''' 26 | 27 | # Handle the options. 28 | try: 29 | opts, args = getopt.getopt(sys.argv[1:], 'hp', ['help', 'pdf']) 30 | except getopt.GetoptError as err: 31 | sys.stderr.write(str(err) + '\n\n') 32 | sys.stderr.write(usage) 33 | sys.exit() 34 | 35 | fmt = 'png' 36 | for o, a in opts: 37 | if o == '-p' or o == '--pdf': 38 | fmt = 'pdf' 39 | else: 40 | sys.stderr.write(usage) 41 | sys.exit() 42 | 43 | # Handle the arguments. 44 | try: 45 | fcns = args[0].split(';') 46 | minx = eval(args[1]) 47 | maxx = eval(args[2]) 48 | except IndexError: 49 | sys.stderr.write(usage) 50 | sys.exit() 51 | 52 | plt.xlim(minx, maxx) 53 | try: 54 | miny = float(args[3]) 55 | maxy = float(args[4]) 56 | plt.ylim(miny, maxy) 57 | except IndexError: 58 | pass 59 | 60 | # Make the plot. 61 | x = linspace(minx, maxx, 100) 62 | for i, f in enumerate(fcns): 63 | y = eval(f) 64 | plt.plot(x, y, "-", linewidth=2) 65 | plt.minorticks_on() 66 | plt.grid(which='both', color='#aaaaaa') 67 | 68 | # Output the plot. 69 | if fmt == 'pdf': 70 | plt.savefig(sys.stdout, format='pdf') 71 | else: 72 | plt.savefig(sys.stdout, format='png') 73 | -------------------------------------------------------------------------------- /cmd/cmdgui/plot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./cmdgui -extraflags="func,string,sin(x);x" "./plot" "{{$.func}} {{$.left}} {{$.right}} {{$.bottom}} {{$.top}}" 3 | 4 | -------------------------------------------------------------------------------- /cmd/cmdgui/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./cmdgui "./plot" "'tan(x); x' {{$.left}} {{$.right}} {{$.top}} {{$.bottom}}" 3 | -------------------------------------------------------------------------------- /cmd/demo/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/TheGrum/renderview/driver" 9 | 10 | "github.com/TheGrum/renderview/examples/mandelbrot" 11 | ) 12 | 13 | func main() { 14 | m := mandelbrot.NewMandelModel() 15 | driver.Main(m) 16 | } 17 | -------------------------------------------------------------------------------- /driver/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | //package driver provides a generic interface to the renderview gui implementations 6 | package driver 7 | 8 | import ( 9 | rv "github.com/TheGrum/renderview" 10 | ) 11 | 12 | // FrameBuffer sets up a full-window rendering method 13 | func FrameBuffer(m rv.RenderModel) { 14 | framebuffer(m) 15 | } 16 | 17 | // Main sets up a window with automatic parameter editing widgets 18 | func Main(m rv.RenderModel) { 19 | main(m) 20 | } 21 | -------------------------------------------------------------------------------- /driver/driver_gio.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build gio 6 | 7 | package driver 8 | 9 | import ( 10 | rv "github.com/TheGrum/renderview" 11 | "github.com/TheGrum/renderview/driver/gio" 12 | ) 13 | 14 | func framebuffer(m rv.RenderModel) { 15 | gio.FrameBuffer(m) 16 | } 17 | 18 | func main(m rv.RenderModel) { 19 | gio.Main(m) 20 | } 21 | -------------------------------------------------------------------------------- /driver/driver_gotk3.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build gotk3,!android,!gtk2,!shiny 6 | 7 | package driver 8 | 9 | import ( 10 | rv "github.com/TheGrum/renderview" 11 | "github.com/TheGrum/renderview/driver/gotk3" 12 | ) 13 | 14 | func framebuffer(m rv.RenderModel) { 15 | gotk3.FrameBuffer(m) 16 | } 17 | 18 | func main(m rv.RenderModel) { 19 | gotk3.Main(m) 20 | } 21 | -------------------------------------------------------------------------------- /driver/driver_gtk2.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build !nogtk2,!gotk3 !nogtk2,!android !nogtk2,!shiny 6 | 7 | package driver 8 | 9 | import ( 10 | rv "github.com/TheGrum/renderview" 11 | "github.com/TheGrum/renderview/driver/gtk2" 12 | ) 13 | 14 | func framebuffer(m rv.RenderModel) { 15 | gtk2.FrameBuffer(m) 16 | } 17 | 18 | func main(m rv.RenderModel) { 19 | gtk2.Main(m) 20 | } 21 | -------------------------------------------------------------------------------- /driver/driver_shiny.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build android shiny 6 | 7 | package driver 8 | 9 | import ( 10 | rv "github.com/TheGrum/renderview" 11 | "github.com/TheGrum/renderview/driver/shiny" 12 | ) 13 | 14 | func framebuffer(m rv.RenderModel) { 15 | shiny.FrameBuffer(m) 16 | } 17 | 18 | func main(m rv.RenderModel) { 19 | shiny.Main(m) 20 | } 21 | -------------------------------------------------------------------------------- /driver/gio/view.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build gio 6 | 7 | package gio 8 | 9 | import ( 10 | "image" 11 | 12 | //"image/color" 13 | "image/draw" 14 | "log" 15 | 16 | rv "github.com/TheGrum/renderview" 17 | 18 | "gioui.org/app" 19 | "gioui.org/f32" 20 | "gioui.org/font/gofont" 21 | "gioui.org/io/key" 22 | "gioui.org/io/pointer" 23 | "gioui.org/io/system" 24 | "gioui.org/layout" 25 | "gioui.org/op/paint" 26 | "gioui.org/widget/material" 27 | 28 | "gioui.org/unit" 29 | "gioui.org/widget" 30 | //"gioui.org/widget/material" 31 | ) 32 | 33 | // FrameBuffer sets up a Gio Window and runs a mainloop rendering the rv.RenderModel 34 | func FrameBuffer(m rv.RenderModel) { 35 | MainLoop(m) 36 | } 37 | 38 | // Main sets up a Gio Window and runs a mainloop rendering the rv.RenderModel with widgets 39 | func Main(m rv.RenderModel) { 40 | //MainLoop(m) 41 | MainLoopWithWidgets(m) 42 | } 43 | 44 | func MainLoop(r rv.RenderModel) { 45 | var needsPaint = false 46 | var mouseIsDown = false 47 | var dragging bool = false 48 | var dx, dy float64 49 | var sx, sy float32 50 | var wx, wy int 51 | 52 | var left, top, right, bottom, width, height, zoom rv.RenderParameter 53 | left = r.GetParameter("left") 54 | top = r.GetParameter("top") 55 | right = r.GetParameter("right") 56 | bottom = r.GetParameter("bottom") 57 | width = r.GetParameter("width") 58 | height = r.GetParameter("height") 59 | zoom = r.GetParameter("zoom") 60 | mouseX := r.GetParameter("mouseX") 61 | mouseY := r.GetParameter("mouseY") 62 | options := r.GetParameter("options") 63 | page := r.GetParameter("page") 64 | // offsetX := r.GetParameter("offsetX") 65 | // offsetY := r.GetParameter("offsetY") 66 | //fmt.Printf("left.GetType() %v", left.GetType()) 67 | leftIsFloat64 := left.GetType() == "float64" 68 | zoomIsFloat64 := zoom.GetType() == "float64" 69 | 70 | w := app.NewWindow() 71 | go func() { 72 | //th := material.NewTheme() 73 | gtx := layout.NewContext(w.Queue()) 74 | for e := range w.Events() { 75 | switch e := e.(type) { 76 | case system.DestroyEvent: 77 | return 78 | 79 | case system.FrameEvent: 80 | wx = e.Size.X 81 | wy = e.Size.Y 82 | width.SetValueInt(e.Size.X) 83 | height.SetValueInt(e.Size.Y) 84 | gtx.Reset(e.Config, e.Size) 85 | img := r.Render() 86 | if img != nil { 87 | ni := paint.NewImageOp(img) 88 | ni.Add(gtx.Ops) 89 | po := paint.PaintOp{f32.Rectangle{f32.Point{0, 0}, f32.Point{float32(img.Bounds().Size().X), float32(img.Bounds().Size().Y)}}} 90 | po.Add(gtx.Ops) 91 | // form(gtx, th) 92 | } 93 | e.Frame(gtx.Ops) 94 | 95 | // case paint.Event: 96 | // w.Upload(image.Point{}, buf, buf.Bounds()) 97 | // w.Publish() 98 | 99 | case key.Event: 100 | if e.Name == "⎋ " { 101 | // return nil 102 | } 103 | if e.Name == "⇞ " { 104 | page.SetValueInt(page.GetValueInt() - 1) 105 | needsPaint = true 106 | } 107 | if e.Name == "⇟ " { 108 | page.SetValueInt(page.GetValueInt() + 1) 109 | needsPaint = true 110 | } 111 | 112 | case pointer.Event: 113 | //fmt.Printf("mouse pos(%v)\n", e) 114 | mouseX.SetValueFloat64(float64(e.Position.X)) 115 | mouseY.SetValueFloat64(float64(e.Position.Y)) 116 | 117 | if mouseIsDown == false && dragging == false && (e.Buttons&pointer.ButtonLeft) == pointer.ButtonLeft { 118 | //fmt.Printf("mouse down left(%v)\n", e) 119 | sx = e.Position.X 120 | sy = e.Position.Y 121 | mouseIsDown = true 122 | } 123 | if e.Scroll.Y > 0 { 124 | if zoomIsFloat64 { 125 | zoom.SetValueFloat64(zoom.GetValueFloat64() - 1) 126 | } else { 127 | zoom.SetValueInt(zoom.GetValueInt() - 1) 128 | } 129 | if options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 130 | mult := 1 + rv.ZOOM_RATE 131 | if leftIsFloat64 { 132 | 133 | zwidth := right.GetValueFloat64() - left.GetValueFloat64() 134 | zheight := bottom.GetValueFloat64() - top.GetValueFloat64() 135 | nzwidth := zwidth * mult 136 | nzheight := zheight * mult 137 | cx := float64(e.Position.X) / float64(wx) 138 | cy := float64(e.Position.Y) / float64(wy) 139 | if options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 140 | cx = 0.5 141 | cy = 0.5 142 | } 143 | //fmt.Printf("zoomOut: mult: %v zwidth: %v nzwidth: %v cx: %v left: %v nleft: %v\n", mult, zwidth, nzwidth, cx, left.GetValueFloat64(), left.GetValueFloat64()-((nzwidth-zwidth)*cx)) 144 | left.SetValueFloat64(left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 145 | top.SetValueFloat64(top.GetValueFloat64() - ((nzheight - zheight) * cy)) 146 | right.SetValueFloat64(left.GetValueFloat64() + nzwidth) 147 | bottom.SetValueFloat64(top.GetValueFloat64() + nzheight) 148 | needsPaint = true 149 | 150 | } 151 | } 152 | 153 | } 154 | if e.Scroll.Y < 0 { 155 | if zoomIsFloat64 { 156 | zoom.SetValueFloat64(zoom.GetValueFloat64() + 1) 157 | } else { 158 | zoom.SetValueInt(zoom.GetValueInt() + 1) 159 | } 160 | if options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 161 | mult := 1 - rv.ZOOM_RATE 162 | if leftIsFloat64 { 163 | zwidth := right.GetValueFloat64() - left.GetValueFloat64() 164 | zheight := bottom.GetValueFloat64() - top.GetValueFloat64() 165 | nzwidth := zwidth * mult 166 | nzheight := zheight * mult 167 | cx := float64(e.Position.X) / float64(wx) 168 | cy := float64(e.Position.Y) / float64(wy) 169 | if options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 170 | cx = 0.5 171 | cy = 0.5 172 | } 173 | left.SetValueFloat64(left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 174 | top.SetValueFloat64(top.GetValueFloat64() - ((nzheight - zheight) * cy)) 175 | right.SetValueFloat64(left.GetValueFloat64() + nzwidth) 176 | bottom.SetValueFloat64(top.GetValueFloat64() + nzheight) 177 | needsPaint = true 178 | } 179 | } 180 | } 181 | if mouseIsDown { 182 | // fmt.Printf("mouse drag(%v) dragging (%v)\n", e, dragging) 183 | if dragging == false { 184 | // fmt.Printf("Checking %v, %v, %v, %v\n", e.Position.X, e.Position.Y, sx, sy) 185 | if ((e.Position.X - sx) > 3) || ((sx - e.Position.X) > 3) || ((e.Position.Y - sy) > 3) || ((sy - e.Position.Y) > 3) { 186 | dragging = true 187 | // fmt.Printf("Dragging.\n") 188 | } 189 | } else { 190 | if leftIsFloat64 { 191 | width := right.GetValueFloat64() - left.GetValueFloat64() 192 | height := bottom.GetValueFloat64() - top.GetValueFloat64() 193 | dx = width / float64(wx) 194 | dy = height / float64(wy) 195 | cx := float64(e.Position.X-sx) * dx 196 | cy := float64(e.Position.Y-sy) * dy 197 | //fmt.Printf("dx %v dy %v cx %v cy %v\n", dx, dy, cx, cy) 198 | left.SetValueFloat64(left.GetValueFloat64() - cx) 199 | right.SetValueFloat64(right.GetValueFloat64() - cx) 200 | top.SetValueFloat64(top.GetValueFloat64() - cy) 201 | bottom.SetValueFloat64(bottom.GetValueFloat64() - cy) 202 | // fmt.Printf("left %v right %v top %v bottom %v", left.GetValueFloat64(), right.GetValueFloat64(), top.GetValueFloat64(), bottom.GetValueFloat64()) 203 | } else { 204 | width := right.GetValueInt() - left.GetValueInt() 205 | height := bottom.GetValueInt() - top.GetValueInt() 206 | dx = float64(width) / float64(wx) 207 | dy = float64(height) / float64(wy) 208 | cx := float64(e.Position.X-sx) * dx 209 | cy := float64(e.Position.Y-sy) * dy 210 | //fmt.Printf("dx %v dy %v cx %v cy %v\n", dx, dy, cx, cy) 211 | left.SetValueInt(int(float64(left.GetValueInt()) - cx)) 212 | right.SetValueInt(int(float64(right.GetValueInt()) - cx)) 213 | top.SetValueInt(int(float64(top.GetValueInt()) - cy)) 214 | bottom.SetValueInt(int(float64(bottom.GetValueInt()) - cy)) 215 | // fmt.Printf("left %v right %v top %v bottom %v", left.GetValueInt(), right.GetValueInt(), top.GetValueInt(), bottom.GetValueInt()) 216 | } 217 | ni := paint.NewImageOp(r.Render()) 218 | ni.Add(gtx.Ops) 219 | po := paint.PaintOp{f32.Rectangle{f32.Point{0, 0}, f32.Point{float32(wx), float32(wy)}}} 220 | po.Add(gtx.Ops) 221 | w.Invalidate() 222 | // Draw(r.Render(), buf.RGBA()) 223 | 224 | sx = e.Position.X 225 | sy = e.Position.Y 226 | 227 | } 228 | } 229 | if e.Buttons == 0 { 230 | dragging = false 231 | mouseIsDown = false 232 | } 233 | 234 | // case size.Event: 235 | // if buf != nil { 236 | // buf.Release() 237 | // } 238 | // r.GetParameter("width").SetValueInt(e.Size().X) 239 | // r.GetParameter("height").SetValueInt(e.Size().Y) 240 | // buf, err = s.NewBuffer(e.Size()) 241 | // if err != nil { 242 | // log.Fatal(err) 243 | // } 244 | // Draw(r.Render(), buf.RGBA()) 245 | default: 246 | 247 | } 248 | if needsPaint { 249 | needsPaint = false 250 | ni := paint.NewImageOp(r.Render()) 251 | ni.Add(gtx.Ops) 252 | po := paint.PaintOp{f32.Rectangle{f32.Point{0, 0}, f32.Point{float32(wx), float32(wy)}}} 253 | po.Add(gtx.Ops) 254 | w.Invalidate() 255 | } 256 | } 257 | }() 258 | 259 | r.SetRequestPaintFunc(func() { 260 | if r == nil || w == nil { 261 | return 262 | } 263 | needsPaint = true 264 | w.Invalidate() 265 | // w.Send(paint.Event{}) 266 | }) 267 | app.Main() 268 | } 269 | 270 | func Draw(mimg image.Image, bimg *image.RGBA) { 271 | if !(mimg == nil) && !(bimg == nil) { 272 | r := mimg.Bounds() 273 | r2 := bimg.Bounds() 274 | mx := r.Dx() 275 | my := r.Dy() 276 | if r2.Dx() < mx { 277 | mx = r2.Dx() 278 | } 279 | if r2.Dy() < my { 280 | my = r2.Dy() 281 | } 282 | r3 := image.Rect(0, 0, mx, my) 283 | draw.Draw(bimg, r3, mimg, image.ZP, draw.Src) 284 | } 285 | } 286 | 287 | func handleError(e error) { 288 | log.Fatal(e) 289 | } 290 | 291 | const SIDEBAR_WIDTH = 160 292 | 293 | func MainLoopWithWidgets(r rv.RenderModel) { 294 | var needsPaint = true 295 | var editorsChanged = true 296 | var mouseIsDown = false 297 | var dragging bool = false 298 | var dx, dy float64 299 | var sx, sy float32 300 | var wx, wy int 301 | 302 | var left, top, right, bottom, width, height, zoom rv.RenderParameter 303 | left = r.GetParameter("left") 304 | top = r.GetParameter("top") 305 | right = r.GetParameter("right") 306 | bottom = r.GetParameter("bottom") 307 | width = r.GetParameter("width") 308 | height = r.GetParameter("height") 309 | zoom = r.GetParameter("zoom") 310 | mouseX := r.GetParameter("mouseX") 311 | mouseY := r.GetParameter("mouseY") 312 | options := r.GetParameter("options") 313 | page := r.GetParameter("page") 314 | sidebarWidth := r.GetParameter("sidebarWidth") 315 | // offsetX := r.GetParameter("offsetX") 316 | // offsetY := r.GetParameter("offsetY") 317 | //fmt.Printf("left.GetType() %v", left.GetType()) 318 | leftIsFloat64 := left.GetType() == "float64" 319 | zoomIsFloat64 := zoom.GetType() == "float64" 320 | paramEditors := []ParamEdit{} 321 | var fullTextEditor ParamEdit 322 | paramList := &layout.List{ 323 | Axis: layout.Vertical, 324 | } 325 | lx := 0 326 | sbw := 0 327 | gofont.Register() 328 | 329 | pnames := r.GetHintedParameterNames(rv.HINT_FULLTEXT) 330 | if len(pnames) > 0 { 331 | param := r.GetParameter(pnames[0]) 332 | fullTextEditor = ParamEdit{ 333 | P: param, 334 | N: new(widget.Editor), 335 | } 336 | fullTextEditor.N.SetText(rv.GetParameterValueAsString(fullTextEditor.P)) 337 | } 338 | for _, pname := range r.GetHintedParameterNamesWithFallback(rv.HINT_SIDEBAR | rv.HINT_FOOTER) { 339 | param := r.GetParameter(pname) 340 | paramEdit := ParamEdit{ 341 | P: param, 342 | N: &widget.Editor{ 343 | SingleLine: true, 344 | }} 345 | paramEditors = append(paramEditors, paramEdit) 346 | paramEdit.N.SetText(rv.GetParameterValueAsString(paramEdit.P)) 347 | } 348 | 349 | w := app.NewWindow() 350 | go func() { 351 | th := material.NewTheme() 352 | gtx := layout.NewContext(w.Queue()) 353 | for e := range w.Events() { 354 | if editorsChanged == true { 355 | // may also need parameters updated 356 | if fullTextEditor.N != nil { 357 | fullTextEditor.N.SetText(rv.GetParameterValueAsString(fullTextEditor.P)) 358 | } 359 | for _, pe := range paramEditors { 360 | pe.N.SetText(rv.GetParameterValueAsString(pe.P)) 361 | } 362 | editorsChanged = false 363 | } 364 | switch e := e.(type) { 365 | case system.DestroyEvent: 366 | return 367 | 368 | case system.FrameEvent: 369 | wx = e.Size.X 370 | wy = e.Size.Y 371 | if len(paramEditors) > 0 { 372 | sbw = sidebarWidth.GetValueInt() 373 | if sbw == 0 { 374 | sbw = SIDEBAR_WIDTH 375 | if fullTextEditor.N == nil { 376 | // just the sidebar - if we have one? 377 | //width.SetValueInt(e.Size.X - sbw) 378 | lx = sbw 379 | } else { 380 | //width.SetValueInt(e.Size.X - sbw - (SIDEBAR_WIDTH * 2)) 381 | lx = sbw + (SIDEBAR_WIDTH * 2) 382 | } 383 | } 384 | } 385 | // fmt.Printf("lx: %v", lx) 386 | width.SetValueInt(e.Size.X) 387 | height.SetValueInt(e.Size.Y) 388 | gtx.Reset(e.Config, e.Size) 389 | gtx.Constraints.Width.Max = sbw 390 | widgetList := []func(){} 391 | widgetList = append(widgetList, func() { 392 | th.Label(unit.Dp(15), "____Parameters____").Layout(gtx) 393 | }) 394 | for _, pe := range paramEditors { 395 | //fmt.Printf("pe: %v %v %v\n", pe, pe.P.GetName(), pe.N.Text()) 396 | widgetList = append(widgetList, func(pe ParamEdit) func() { 397 | return func() { 398 | th.Label(unit.Dp(15), pe.P.GetName()).Layout(gtx) 399 | } 400 | }(pe)) 401 | widgetList = append(widgetList, func(pe ParamEdit) func() { 402 | return func() { 403 | th.Editor(pe.P.GetName()).Layout(gtx, pe.N) 404 | for range pe.N.Events(gtx) { 405 | rv.SetParameterValueFromString(pe.P, pe.N.Text()) 406 | } 407 | } 408 | }(pe)) 409 | } 410 | paramList.Layout(gtx, len(widgetList), func(i int) { 411 | layout.UniformInset(unit.Dp(1)).Layout(gtx, widgetList[i]) 412 | }) 413 | //gtx.Reset(e.Config, e.Size) 414 | if fullTextEditor.N != nil { 415 | gtx.Constraints.Width.Max = sbw + (SIDEBAR_WIDTH * 2) - 5 416 | layout.Inset{Top: unit.Dp(2), Left: unit.Dp(float32(sbw + 5))}.Layout(gtx, func() { 417 | layout.Stack{Alignment: layout.N}.Layout(gtx, 418 | // layout.Stacked(func() { 419 | // th.Label(unit.Dp(15), fullTextEditor.P.GetName()).Layout(gtx) 420 | // }), 421 | layout.Expanded(func() { 422 | th.Editor(fullTextEditor.P.GetName()).Layout(gtx, fullTextEditor.N) 423 | //fullTextEditor.N.SetText(fullTextEditor.P.GetValueString()) 424 | for range fullTextEditor.N.Events(gtx) { 425 | rv.SetParameterValueFromString(fullTextEditor.P, fullTextEditor.N.Text()) 426 | } 427 | })) 428 | }) 429 | } 430 | //gtx.Reset(e.Config, e.Size) 431 | gtx.Constraints.Width.Max = e.Size.X 432 | img := r.Render() 433 | if img != nil { 434 | ni := paint.NewImageOp(img) 435 | ni.Add(gtx.Ops) 436 | po := paint.PaintOp{f32.Rectangle{f32.Point{float32(lx), 0}, f32.Point{float32(img.Bounds().Size().X), float32(img.Bounds().Size().Y)}}} 437 | po.Add(gtx.Ops) 438 | } 439 | 440 | needsPaint = false 441 | 442 | e.Frame(gtx.Ops) 443 | 444 | case key.Event: 445 | if e.Name == "⎋ " { 446 | // return nil 447 | } 448 | if e.Name == "⇞ " { 449 | page.SetValueInt(page.GetValueInt() - 1) 450 | editorsChanged = true 451 | needsPaint = true 452 | } 453 | if e.Name == "⇟ " { 454 | page.SetValueInt(page.GetValueInt() + 1) 455 | editorsChanged = true 456 | needsPaint = true 457 | } 458 | 459 | case pointer.Event: 460 | //fmt.Printf("mouse pos(%v)\n", e) 461 | mouseX.SetValueFloat64(float64(e.Position.X)) 462 | mouseY.SetValueFloat64(float64(e.Position.Y)) 463 | 464 | if e.Position.X <= float32(lx) { 465 | break 466 | } 467 | if mouseIsDown == false && dragging == false && (e.Buttons&pointer.ButtonLeft) == pointer.ButtonLeft { 468 | //fmt.Printf("mouse down left(%v)\n", e) 469 | sx = e.Position.X 470 | sy = e.Position.Y 471 | mouseIsDown = true 472 | } 473 | if e.Scroll.Y > 0 { 474 | if zoomIsFloat64 { 475 | zoom.SetValueFloat64(zoom.GetValueFloat64() - 1) 476 | } else { 477 | zoom.SetValueInt(zoom.GetValueInt() - 1) 478 | } 479 | if options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 480 | mult := 1 + rv.ZOOM_RATE 481 | if leftIsFloat64 { 482 | 483 | zwidth := right.GetValueFloat64() - left.GetValueFloat64() 484 | zheight := bottom.GetValueFloat64() - top.GetValueFloat64() 485 | nzwidth := zwidth * mult 486 | nzheight := zheight * mult 487 | cx := float64(e.Position.X) / float64(wx) 488 | cy := float64(e.Position.Y) / float64(wy) 489 | if options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 490 | cx = 0.5 491 | cy = 0.5 492 | } 493 | //fmt.Printf("zoomOut: mult: %v zwidth: %v nzwidth: %v cx: %v left: %v nleft: %v\n", mult, zwidth, nzwidth, cx, left.GetValueFloat64(), left.GetValueFloat64()-((nzwidth-zwidth)*cx)) 494 | left.SetValueFloat64(left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 495 | top.SetValueFloat64(top.GetValueFloat64() - ((nzheight - zheight) * cy)) 496 | right.SetValueFloat64(left.GetValueFloat64() + nzwidth) 497 | bottom.SetValueFloat64(top.GetValueFloat64() + nzheight) 498 | needsPaint = true 499 | } 500 | } 501 | editorsChanged = true 502 | } 503 | if e.Scroll.Y < 0 { 504 | if zoomIsFloat64 { 505 | zoom.SetValueFloat64(zoom.GetValueFloat64() + 1) 506 | } else { 507 | zoom.SetValueInt(zoom.GetValueInt() + 1) 508 | } 509 | if options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 510 | mult := 1 - rv.ZOOM_RATE 511 | if leftIsFloat64 { 512 | zwidth := right.GetValueFloat64() - left.GetValueFloat64() 513 | zheight := bottom.GetValueFloat64() - top.GetValueFloat64() 514 | nzwidth := zwidth * mult 515 | nzheight := zheight * mult 516 | cx := float64(e.Position.X) / float64(wx) 517 | cy := float64(e.Position.Y) / float64(wy) 518 | if options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 519 | cx = 0.5 520 | cy = 0.5 521 | } 522 | left.SetValueFloat64(left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 523 | top.SetValueFloat64(top.GetValueFloat64() - ((nzheight - zheight) * cy)) 524 | right.SetValueFloat64(left.GetValueFloat64() + nzwidth) 525 | bottom.SetValueFloat64(top.GetValueFloat64() + nzheight) 526 | needsPaint = true 527 | } 528 | } 529 | editorsChanged = true 530 | } 531 | if mouseIsDown { 532 | // fmt.Printf("mouse drag(%v) dragging (%v)\n", e, dragging) 533 | if dragging == false { 534 | // fmt.Printf("Checking %v, %v, %v, %v\n", e.Position.X, e.Position.Y, sx, sy) 535 | if ((e.Position.X - sx) > 3) || ((sx - e.Position.X) > 3) || ((e.Position.Y - sy) > 3) || ((sy - e.Position.Y) > 3) { 536 | dragging = true 537 | // fmt.Printf("Dragging.\n") 538 | } 539 | } else { 540 | if leftIsFloat64 { 541 | width := right.GetValueFloat64() - left.GetValueFloat64() 542 | height := bottom.GetValueFloat64() - top.GetValueFloat64() 543 | dx = width / float64(wx) 544 | dy = height / float64(wy) 545 | cx := float64(e.Position.X-sx) * dx 546 | cy := float64(e.Position.Y-sy) * dy 547 | //fmt.Printf("dx %v dy %v cx %v cy %v\n", dx, dy, cx, cy) 548 | left.SetValueFloat64(left.GetValueFloat64() - cx) 549 | right.SetValueFloat64(right.GetValueFloat64() - cx) 550 | top.SetValueFloat64(top.GetValueFloat64() - cy) 551 | bottom.SetValueFloat64(bottom.GetValueFloat64() - cy) 552 | // fmt.Printf("left %v right %v top %v bottom %v", left.GetValueFloat64(), right.GetValueFloat64(), top.GetValueFloat64(), bottom.GetValueFloat64()) 553 | } else { 554 | width := right.GetValueInt() - left.GetValueInt() 555 | height := bottom.GetValueInt() - top.GetValueInt() 556 | dx = float64(width) / float64(wx) 557 | dy = float64(height) / float64(wy) 558 | cx := float64(e.Position.X-sx) * dx 559 | cy := float64(e.Position.Y-sy) * dy 560 | //fmt.Printf("dx %v dy %v cx %v cy %v\n", dx, dy, cx, cy) 561 | left.SetValueInt(int(float64(left.GetValueInt()) - cx)) 562 | right.SetValueInt(int(float64(right.GetValueInt()) - cx)) 563 | top.SetValueInt(int(float64(top.GetValueInt()) - cy)) 564 | bottom.SetValueInt(int(float64(bottom.GetValueInt()) - cy)) 565 | // fmt.Printf("left %v right %v top %v bottom %v", left.GetValueInt(), right.GetValueInt(), top.GetValueInt(), bottom.GetValueInt()) 566 | } 567 | editorsChanged = true 568 | needsPaint = true 569 | // ni := paint.NewImageOp(r.Render()) 570 | // ni.Add(gtx.Ops) 571 | // po := paint.PaintOp{f32.Rectangle{f32.Point{0, 0}, f32.Point{float32(wx), float32(wy)}}} 572 | // po.Add(gtx.Ops) 573 | // w.Invalidate() 574 | // Draw(r.Render(), buf.RGBA()) 575 | 576 | sx = e.Position.X 577 | sy = e.Position.Y 578 | 579 | } 580 | } 581 | if e.Buttons == 0 { 582 | dragging = false 583 | mouseIsDown = false 584 | } 585 | 586 | // case size.Event: 587 | // if buf != nil { 588 | // buf.Release() 589 | // } 590 | // r.GetParameter("width").SetValueInt(e.Size().X) 591 | // r.GetParameter("height").SetValueInt(e.Size().Y) 592 | // buf, err = s.NewBuffer(e.Size()) 593 | // if err != nil { 594 | // log.Fatal(err) 595 | // } 596 | // Draw(r.Render(), buf.RGBA()) 597 | default: 598 | 599 | } 600 | } 601 | }() 602 | 603 | r.SetRequestPaintFunc(func() { 604 | if r == nil || w == nil { 605 | return 606 | } 607 | needsPaint = true 608 | w.Invalidate() 609 | // w.Send(paint.Event{}) 610 | }) 611 | app.Main() 612 | } 613 | 614 | type ParamEdit struct { 615 | P rv.RenderParameter 616 | N *widget.Editor 617 | } 618 | -------------------------------------------------------------------------------- /driver/gotk3/view.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build gotk3 6 | 7 | package gotk3 8 | 9 | import ( 10 | "image" 11 | "image/draw" 12 | "log" 13 | 14 | rv "github.com/TheGrum/renderview" 15 | 16 | "github.com/gotk3/gotk3/cairo" 17 | "github.com/gotk3/gotk3/gdk" 18 | "github.com/gotk3/gotk3/gtk" 19 | ) 20 | 21 | // FrameBuffer sets up a GTK3 Window and runs a mainloop rendering the RenderModel 22 | func FrameBuffer(m rv.RenderModel) { 23 | GtkWindowInit(m) 24 | } 25 | 26 | // Main sets up a GTK3 Window with widgets for editing parameters and runs a 27 | // mainloop rendering the RenderModel 28 | func Main(m rv.RenderModel) { 29 | GtkWindowWithWidgetsInit(m) 30 | } 31 | 32 | func GtkWindowInit(r rv.RenderModel) { 33 | gtk.Init(nil) 34 | window := GetGtkWindow(r, false) 35 | window.Connect("destroy", func() { 36 | gtk.MainQuit() 37 | }) 38 | 39 | window.SetDefaultSize(400, 400) 40 | window.ShowAll() 41 | gtk.Main() 42 | } 43 | 44 | func GtkWindowWithWidgetsInit(r rv.RenderModel) { 45 | gtk.Init(nil) 46 | window := GetGtkWindow(r, true) 47 | window.Connect("destroy", func() { 48 | gtk.MainQuit() 49 | }) 50 | 51 | window.SetSizeRequest(400, 400) 52 | window.ShowAll() 53 | gtk.Main() 54 | } 55 | 56 | func GetGtkWindow(r rv.RenderModel, addWidgets bool) *gtk.Window { 57 | window, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) 58 | if err != nil { 59 | log.Fatal("Unable to create window:", err) 60 | } 61 | var child gtk.IWidget 62 | render := NewGtkRenderWidget(r) 63 | child = render 64 | if addWidgets { 65 | child = WrapRenderWidget(render) 66 | } 67 | window.Add(child) 68 | return window 69 | } 70 | 71 | func WrapRenderWidget(r *GtkRenderWidget) gtk.IWidget { 72 | parent, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 1) 73 | sidebar, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 1) 74 | names := r.R.GetHintedParameterNamesWithFallback(rv.HINT_SIDEBAR | rv.HINT_FOOTER) 75 | if len(names) > 0 { 76 | label, _ := gtk.LabelNew("________Parameters________") 77 | sidebar.PackStart(label, false, false, 1) 78 | 79 | for i := 0; i < len(names); i++ { 80 | label, _ = gtk.LabelNew(names[i]) 81 | sidebar.PackStart(label, false, false, 1) 82 | tv := NewGtkParamWidget(r.R.GetParameter(names[i]), r) 83 | r.ParamWidgets = append(r.ParamWidgets, tv) 84 | sidebar.PackStart(tv, false, false, 1) 85 | } 86 | 87 | parent.PackStart(sidebar, false, true, 0) 88 | } 89 | names = r.R.GetHintedParameterNames(rv.HINT_FULLTEXT) 90 | if len(names) > 0 { 91 | // we can only do this for one parameter, so ignore multiples 92 | tv := NewGtkParamWidget(r.R.GetParameter(names[0]), r) 93 | r.ParamWidgets = append(r.ParamWidgets, tv) 94 | parent.PackStart(tv, true, true, 1) 95 | } 96 | parent.PackEnd(r, true, true, 2) 97 | return parent 98 | } 99 | 100 | type GtkRenderWidget struct { 101 | *gtk.DrawingArea 102 | 103 | pixbuf *gdk.Pixbuf 104 | Image image.Image 105 | 106 | index int 107 | R rv.RenderModel 108 | 109 | left, 110 | top, 111 | right, 112 | bottom, 113 | width, 114 | height, 115 | zoom, 116 | mouseX, 117 | mouseY, 118 | options, 119 | page, 120 | zoomRate rv.RenderParameter 121 | 122 | sx, 123 | sy float64 124 | 125 | leftIsFloat64, 126 | zoomIsFloat64, 127 | mouseIsDown, 128 | dragging, 129 | needsPaint bool 130 | needsUpdate bool 131 | 132 | ParamWidgets []*GtkParamWidget 133 | } 134 | 135 | func NewGtkRenderWidget(r rv.RenderModel) *GtkRenderWidget { 136 | i, err := gtk.DrawingAreaNew() 137 | handleError(err) 138 | w := &GtkRenderWidget{ 139 | DrawingArea: i, 140 | R: r, 141 | } 142 | w.left = r.GetParameter("left") 143 | w.top = r.GetParameter("top") 144 | w.right = r.GetParameter("right") 145 | w.bottom = r.GetParameter("bottom") 146 | w.width = r.GetParameter("width") 147 | w.height = r.GetParameter("height") 148 | w.zoom = r.GetParameter("zoom") 149 | w.mouseX = r.GetParameter("mouseX") 150 | w.mouseY = r.GetParameter("mouseY") 151 | w.options = r.GetParameter("options") 152 | w.page = r.GetParameter("page") 153 | w.zoomRate = r.GetParameter("zoomRate") 154 | w.leftIsFloat64 = w.left.GetType() == "float64" 155 | w.zoomIsFloat64 = w.zoom.GetType() == "float64" 156 | w.Connect("draw", w.Draw) 157 | w.Connect("configure-event", w.Configure) 158 | w.Connect("motion-notify-event", w.OnMotion) 159 | w.Connect("button-press-event", w.OnButton) 160 | w.Connect("button-release-event", w.OnButton) 161 | w.Connect("scroll-event", w.OnScroll) 162 | w.Connect("key-press-event", w.OnKeyPress) 163 | w.R.SetRequestPaintFunc(func() { 164 | //w.UpdateParamWidgets() 165 | w.needsUpdate = true 166 | w.QueueDraw() 167 | //w.GetWindow().Invalidate(nil, false) 168 | }) 169 | w.SetCanFocus(true) 170 | // w.SetFocusOnClick(true) // missing? 171 | w.SetEvents(int(gdk.POINTER_MOTION_MASK | gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK | gdk.EXPOSURE_MASK | gdk.SCROLL_MASK | gdk.KEY_PRESS_MASK)) 172 | return w 173 | } 174 | 175 | func (w *GtkRenderWidget) SetNeedsPaint() { 176 | w.needsPaint = true 177 | w.QueueDraw() 178 | } 179 | 180 | func (w *GtkRenderWidget) UpdateParamWidgets() { 181 | w.R.Lock() 182 | defer w.R.Unlock() 183 | for i := 0; i < len(w.ParamWidgets); i++ { 184 | w.ParamWidgets[i].Update() 185 | } 186 | w.needsUpdate = false 187 | } 188 | 189 | func (w *GtkRenderWidget) Configure() { 190 | allocation := w.GetAllocation() 191 | w.width.SetValueInt(allocation.GetWidth()) 192 | w.height.SetValueInt(allocation.GetHeight()) 193 | 194 | var err error 195 | w.pixbuf, err = gdk.PixbufNew(gdk.COLORSPACE_RGB, true, 8, allocation.GetWidth(), allocation.GetHeight()) 196 | if err != nil { 197 | log.Fatal(err) 198 | } 199 | w.needsPaint = true 200 | w.needsUpdate = true 201 | } 202 | 203 | func (w *GtkRenderWidget) Draw(da *gtk.DrawingArea, cr *cairo.Context) { 204 | if w.needsUpdate { 205 | w.UpdateParamWidgets() 206 | } 207 | if w.needsPaint || w.Image == nil { 208 | img := w.R.Render() 209 | if img == nil { 210 | return 211 | } 212 | switch a := img.(type) { 213 | case *image.RGBA: 214 | w.Image = a 215 | w.needsPaint = false 216 | case *image.NRGBA: 217 | w.Image = a 218 | w.needsPaint = false 219 | default: 220 | i2 := image.NewRGBA(img.Bounds()) 221 | draw.Draw(i2, img.Bounds(), img, image.ZP, draw.Src) 222 | w.Image = i2 223 | w.needsPaint = false 224 | } 225 | } 226 | // copy from Go image to GDK image 227 | //w.pixbuf.Fill(0) 228 | switch a := w.Image.(type) { 229 | case *image.RGBA: 230 | GdkPixelCopy(a, w.pixbuf, image.ZR, image.ZP) 231 | case *image.NRGBA: 232 | GdkPixelCopyNRGBA(a, w.pixbuf, image.ZR, image.ZP) 233 | } 234 | 235 | gtk.GdkCairoSetSourcePixBuf(cr, w.pixbuf, 0, 0) 236 | cr.Paint() 237 | 238 | var err error 239 | allocation := w.GetAllocation() 240 | w.pixbuf, err = gdk.PixbufNew(gdk.COLORSPACE_RGB, true, 8, allocation.GetWidth(), allocation.GetHeight()) 241 | if err != nil { 242 | log.Fatal(err) 243 | } 244 | } 245 | 246 | func GdkPixelCopy(source *image.RGBA, target *gdk.Pixbuf, region image.Rectangle, targetOffset image.Point) { 247 | if source == nil { 248 | return 249 | } 250 | if target == nil { 251 | return 252 | } 253 | sourceBounds := source.Rect 254 | targetBounds := image.Rect(0, 0, target.GetWidth(), target.GetHeight()) 255 | sourceRowstride := source.Stride 256 | targetRowstride := target.GetRowstride() 257 | //Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*4] 258 | var targetPix []byte 259 | var sourcePix []uint8 260 | targetPix = target.GetPixels() 261 | sourcePix = source.Pix 262 | 263 | if region.Dx() == 0 { 264 | region = sourceBounds.Intersect(targetBounds.Sub(targetOffset)) 265 | } 266 | region = region.Intersect(sourceBounds).Intersect(targetBounds.Sub(targetOffset)) 267 | var x, y int 268 | 269 | for y = region.Min.Y; y < region.Max.Y; y++ { 270 | for x = region.Min.X; x < region.Max.X; x++ { 271 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4] = sourcePix[y*sourceRowstride+x*4] 272 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+1] = sourcePix[y*sourceRowstride+x*4+1] 273 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+2] = sourcePix[y*sourceRowstride+x*4+2] 274 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+3] = sourcePix[y*sourceRowstride+x*4+3] 275 | } 276 | } 277 | } 278 | 279 | func GdkPixelCopyNRGBA(source *image.NRGBA, target *gdk.Pixbuf, region image.Rectangle, targetOffset image.Point) { 280 | if source == nil { 281 | return 282 | } 283 | if target == nil { 284 | return 285 | } 286 | sourceBounds := source.Rect 287 | targetBounds := image.Rect(0, 0, target.GetWidth(), target.GetHeight()) 288 | sourceRowstride := source.Stride 289 | targetRowstride := target.GetRowstride() 290 | //Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*4] 291 | var targetPix []byte 292 | var sourcePix []uint8 293 | targetPix = target.GetPixels() 294 | sourcePix = source.Pix 295 | 296 | if region.Dx() == 0 { 297 | region = sourceBounds.Intersect(targetBounds.Sub(targetOffset)) 298 | } 299 | region = region.Intersect(sourceBounds).Intersect(targetBounds.Sub(targetOffset)) 300 | var x, y int 301 | 302 | for y = region.Min.Y; y < region.Max.Y; y++ { 303 | for x = region.Min.X; x < region.Max.X; x++ { 304 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4] = sourcePix[y*sourceRowstride+x*4] 305 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+1] = sourcePix[y*sourceRowstride+x*4+1] 306 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+2] = sourcePix[y*sourceRowstride+x*4+2] 307 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+3] = sourcePix[y*sourceRowstride+x*4+3] 308 | } 309 | } 310 | } 311 | 312 | func (w *GtkRenderWidget) OnScroll(da *gtk.DrawingArea, ge *gdk.Event) { 313 | e := &gdk.EventScroll{ge} 314 | if e.Direction() == gdk.SCROLL_DOWN { 315 | if w.zoomIsFloat64 { 316 | w.zoom.SetValueFloat64(w.zoom.GetValueFloat64() - 1) 317 | } else { 318 | w.zoom.SetValueInt(w.zoom.GetValueInt() - 1) 319 | } 320 | if w.options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || w.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 321 | mult := 1 + rv.ZOOM_RATE 322 | if w.zoomRate.GetValueFloat64() > 0 { 323 | mult = 1 + w.zoomRate.GetValueFloat64() 324 | } 325 | if w.leftIsFloat64 { 326 | 327 | zwidth := w.right.GetValueFloat64() - w.left.GetValueFloat64() 328 | zheight := w.bottom.GetValueFloat64() - w.top.GetValueFloat64() 329 | nzwidth := zwidth * mult 330 | nzheight := zheight * mult 331 | cx := float64(e.X()) / float64(w.width.GetValueInt()) 332 | cy := float64(e.Y()) / float64(w.height.GetValueInt()) 333 | if w.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 334 | cx = 0.5 335 | cy = 0.5 336 | } 337 | //fmt.Printf("zoomOut: mult: %v zwidth: %v nzwidth: %v cx: %v left: %v nleft: %v\n", mult, zwidth, nzwidth, cx, left.GetValueFloat64(), left.GetValueFloat64()-((nzwidth-zwidth)*cx)) 338 | w.left.SetValueFloat64(w.left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 339 | w.top.SetValueFloat64(w.top.GetValueFloat64() - ((nzheight - zheight) * cy)) 340 | w.right.SetValueFloat64(w.left.GetValueFloat64() + nzwidth) 341 | w.bottom.SetValueFloat64(w.top.GetValueFloat64() + nzheight) 342 | } 343 | } 344 | 345 | //if gdk.ScrollDirection(e.Direction) == gdk.SCROLL_UP { 346 | } else { 347 | if w.zoomIsFloat64 { 348 | w.zoom.SetValueFloat64(w.zoom.GetValueFloat64() + 1) 349 | } else { 350 | w.zoom.SetValueInt(w.zoom.GetValueInt() + 1) 351 | } 352 | if w.options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || w.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 353 | mult := 1 - rv.ZOOM_RATE 354 | if w.zoomRate.GetValueFloat64() > 0 { 355 | mult = 1 - w.zoomRate.GetValueFloat64() 356 | } 357 | if w.leftIsFloat64 { 358 | zwidth := w.right.GetValueFloat64() - w.left.GetValueFloat64() 359 | zheight := w.bottom.GetValueFloat64() - w.top.GetValueFloat64() 360 | nzwidth := zwidth * mult 361 | nzheight := zheight * mult 362 | cx := float64(e.X()) / float64(w.width.GetValueInt()) 363 | cy := float64(e.Y()) / float64(w.height.GetValueInt()) 364 | if w.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 365 | cx = 0.5 366 | cy = 0.5 367 | } 368 | w.left.SetValueFloat64(w.left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 369 | w.top.SetValueFloat64(w.top.GetValueFloat64() - ((nzheight - zheight) * cy)) 370 | w.right.SetValueFloat64(w.left.GetValueFloat64() + nzwidth) 371 | w.bottom.SetValueFloat64(w.top.GetValueFloat64() + nzheight) 372 | } 373 | } 374 | } 375 | w.needsUpdate = true 376 | w.SetNeedsPaint() 377 | 378 | } 379 | 380 | func (w *GtkRenderWidget) OnMotion(da *gtk.DrawingArea, ge *gdk.Event) { 381 | e := &gdk.EventMotion{ge} 382 | // fmt.Printf("Motion: X:%v Y:%v sx:%v sy:%v mouseIsDown:%v dragging:%v\n", e.X, e.Y, w.sx, w.sy, w.mouseIsDown, w.dragging) 383 | var X, Y float64 384 | X, Y = e.MotionVal() 385 | w.mouseX.SetValueFloat64(X) 386 | w.mouseY.SetValueFloat64(Y) 387 | 388 | if w.mouseIsDown && w.dragging == false { 389 | if ((X - w.sx) > 3) || ((w.sx - X) > 3) || ((Y - w.sy) > 3) || ((w.sy - Y) > 3) { 390 | w.dragging = true 391 | } 392 | } 393 | 394 | if w.dragging { 395 | if w.leftIsFloat64 { 396 | width := w.right.GetValueFloat64() - w.left.GetValueFloat64() 397 | height := w.bottom.GetValueFloat64() - w.top.GetValueFloat64() 398 | dx := width / float64(w.width.GetValueInt()) 399 | dy := height / float64(w.height.GetValueInt()) 400 | cx := float64(X-w.sx) * dx 401 | cy := float64(Y-w.sy) * dy 402 | // fmt.Printf("dx %v dy %v cx %v cy %v\n", dx, dy, cx, cy) 403 | w.left.SetValueFloat64(w.left.GetValueFloat64() - cx) 404 | w.right.SetValueFloat64(w.right.GetValueFloat64() - cx) 405 | w.top.SetValueFloat64(w.top.GetValueFloat64() - cy) 406 | w.bottom.SetValueFloat64(w.bottom.GetValueFloat64() - cy) 407 | } else { 408 | width := w.right.GetValueInt() - w.left.GetValueInt() 409 | height := w.bottom.GetValueInt() - w.top.GetValueInt() 410 | dx := float64(width) / float64(w.width.GetValueInt()) 411 | dy := float64(height) / float64(w.height.GetValueInt()) 412 | cx := float64(X-w.sx) * dx 413 | cy := float64(Y-w.sy) * dy 414 | w.left.SetValueInt(int(float64(w.left.GetValueInt()) - cx)) 415 | w.right.SetValueInt(int(float64(w.right.GetValueInt()) - cx)) 416 | w.top.SetValueInt(int(float64(w.top.GetValueInt()) - cy)) 417 | w.bottom.SetValueInt(int(float64(w.bottom.GetValueInt()) - cy)) 418 | } 419 | // Draw(r.Render(), buf.RGBA()) 420 | w.needsUpdate = true 421 | w.SetNeedsPaint() 422 | 423 | w.sx = X 424 | w.sy = Y 425 | 426 | } 427 | 428 | } 429 | 430 | func (w *GtkRenderWidget) OnButton(da *gtk.DrawingArea, ge *gdk.Event) { 431 | e := &gdk.EventButton{ge} 432 | // fmt.Printf("Button called with %v\n", e)i 433 | w.mouseX.SetValueFloat64(e.X()) 434 | w.mouseY.SetValueFloat64(e.Y()) 435 | w.GrabFocus() 436 | 437 | if gdk.EventType(e.Type()) == gdk.EVENT_BUTTON_PRESS { 438 | if w.dragging == false && w.mouseIsDown == false && e.Button() == 1 { 439 | // fmt.Println("Mousedown") 440 | w.sx = e.X() 441 | w.sy = e.Y() 442 | w.mouseIsDown = true 443 | } 444 | } 445 | if gdk.EventType(e.Type()) == gdk.EVENT_BUTTON_RELEASE { 446 | // fmt.Println("Mouseup") 447 | w.mouseIsDown = false 448 | w.dragging = false 449 | } 450 | } 451 | 452 | const ( 453 | PAGE_UP uint = 0xff55 454 | PAGE_DOWN uint = 0xff56 455 | ) 456 | 457 | func (w *GtkRenderWidget) OnKeyPress(da *gtk.DrawingArea, ge *gdk.Event) { 458 | e := &gdk.EventKey{ge} 459 | if e.KeyVal() == PAGE_UP { 460 | w.page.SetValueInt(w.page.GetValueInt() - 1) 461 | w.needsUpdate = true 462 | w.SetNeedsPaint() 463 | } 464 | if e.KeyVal() == PAGE_DOWN { 465 | w.page.SetValueInt(w.page.GetValueInt() + 1) 466 | w.needsUpdate = true 467 | w.SetNeedsPaint() 468 | } 469 | } 470 | 471 | type GtkParamWidget struct { 472 | gtk.IWidget 473 | P rv.RenderParameter 474 | } 475 | 476 | func NewGtkParamWidget(p rv.RenderParameter, w *GtkRenderWidget) *GtkParamWidget { 477 | tv, err := gtk.TextViewNew() 478 | if err != nil { 479 | log.Fatal(err) 480 | } 481 | r := &GtkParamWidget{ 482 | IWidget: tv, 483 | P: p, 484 | } 485 | tv.SetEditable(true) 486 | tv.SetCursorVisible(true) 487 | tb, err := tv.GetBuffer() 488 | if err != nil { 489 | log.Fatal(err) 490 | } 491 | tb.SetText(rv.GetParameterValueAsString(p)) 492 | tb.Connect("changed", func() { 493 | var start, end *gtk.TextIter 494 | start, end = tb.GetBounds() 495 | s, err := tb.GetText(start, end, false) 496 | if err != nil { 497 | log.Fatal(err) 498 | } 499 | pValue := rv.GetParameterValueAsString(r.P) 500 | if s != pValue { 501 | rv.SetParameterValueFromString(r.P, s) 502 | } 503 | w.SetNeedsPaint() 504 | }) 505 | return r 506 | } 507 | 508 | func (w *GtkParamWidget) Update() { 509 | switch a := w.IWidget.(type) { 510 | case *gtk.TextView: 511 | tb, err := a.GetBuffer() 512 | if err != nil { 513 | log.Fatal(err) 514 | } 515 | tb.SetText(rv.GetParameterValueAsString(w.P)) 516 | } 517 | } 518 | 519 | func handleError(err error) { 520 | if err != nil { 521 | log.Fatal(err) 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /driver/gtk2/view.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build !gotk3,!nogtk2 !shiny,!nogtk2 6 | 7 | package gtk2 8 | 9 | import ( 10 | "image" 11 | "image/draw" 12 | "unsafe" 13 | 14 | rv "github.com/TheGrum/renderview" 15 | 16 | "github.com/mattn/go-gtk/gdk" 17 | "github.com/mattn/go-gtk/gdkpixbuf" 18 | "github.com/mattn/go-gtk/glib" 19 | "github.com/mattn/go-gtk/gtk" 20 | ) 21 | 22 | // FrameBuffer sets up a GTK2 Window and runs a mainloop rendering the RenderModel 23 | func FrameBuffer(m rv.RenderModel) { 24 | GtkWindowInit(m) 25 | } 26 | 27 | // Main sets up a GTK2 Window with widgets for editing parameters and runs a 28 | // mainloop rendering the RenderModel 29 | func Main(m rv.RenderModel) { 30 | GtkWindowWithWidgetsInit(m) 31 | } 32 | 33 | func GtkWindowInit(r rv.RenderModel) { 34 | gtk.Init(nil) 35 | window := GetGtkWindow(r, false) 36 | window.Connect("destroy", func(ctx *glib.CallbackContext) { 37 | // println("got destroy!", ctx.Data().(string)) 38 | gtk.MainQuit() 39 | }, "foo") 40 | 41 | window.SetSizeRequest(400, 400) 42 | window.ShowAll() 43 | gtk.Main() 44 | } 45 | 46 | func GtkWindowWithWidgetsInit(r rv.RenderModel) { 47 | gtk.Init(nil) 48 | window := GetGtkWindow(r, true) 49 | window.Connect("destroy", func(ctx *glib.CallbackContext) { 50 | // println("got destroy!", ctx.Data().(string)) 51 | gtk.MainQuit() 52 | }, "foo") 53 | 54 | window.SetSizeRequest(400, 400) 55 | window.ShowAll() 56 | gtk.Main() 57 | } 58 | 59 | func GetGtkWindow(r rv.RenderModel, addWidgets bool) *gtk.Window { 60 | window := gtk.NewWindow(gtk.WINDOW_TOPLEVEL) 61 | var child gtk.IWidget 62 | render := NewGtkRenderWidget(r) 63 | child = render 64 | if addWidgets { 65 | child = WrapRenderWidget(render) 66 | } 67 | window.Add(child) 68 | return window 69 | } 70 | 71 | func WrapRenderWidget(r *GtkRenderWidget) gtk.IWidget { 72 | parent := gtk.NewHBox(false, 1) 73 | sidebar := gtk.NewVBox(false, 1) 74 | names := r.R.GetHintedParameterNamesWithFallback(rv.HINT_SIDEBAR | rv.HINT_FOOTER) 75 | if len(names) > 0 { 76 | sidebar.PackStart(gtk.NewLabel("________Parameters________"), false, false, 1) 77 | 78 | for i := 0; i < len(names); i++ { 79 | sidebar.PackStart(gtk.NewLabel(names[i]), false, false, 1) 80 | tv := NewGtkParamWidget(r.R.GetParameter(names[i]), r) 81 | r.ParamWidgets = append(r.ParamWidgets, tv) 82 | sidebar.PackStart(tv, false, false, 1) 83 | } 84 | 85 | parent.PackStart(sidebar, false, true, 0) 86 | } 87 | names = r.R.GetHintedParameterNames(rv.HINT_FULLTEXT) 88 | if len(names) > 0 { 89 | // we can only do this for one parameter, so ignore multiples 90 | tv := NewGtkParamWidget(r.R.GetParameter(names[0]), r) 91 | r.ParamWidgets = append(r.ParamWidgets, tv) 92 | parent.PackStart(tv, true, true, 1) 93 | } 94 | parent.PackEnd(r, true, true, 2) 95 | return parent 96 | } 97 | 98 | type GtkRenderWidget struct { 99 | *gtk.DrawingArea 100 | 101 | pixbuf *gdkpixbuf.Pixbuf 102 | Image image.Image 103 | 104 | index int 105 | R rv.RenderModel 106 | 107 | left, 108 | top, 109 | right, 110 | bottom, 111 | width, 112 | height, 113 | zoom, 114 | mouseX, 115 | mouseY, 116 | options, 117 | page, 118 | zoomRate rv.RenderParameter 119 | 120 | sx, 121 | sy float64 122 | 123 | leftIsFloat64, 124 | zoomIsFloat64, 125 | mouseIsDown, 126 | dragging, 127 | needsPaint bool 128 | needsUpdate bool 129 | 130 | ParamWidgets []*GtkParamWidget 131 | } 132 | 133 | func NewGtkRenderWidget(r rv.RenderModel) *GtkRenderWidget { 134 | w := &GtkRenderWidget{ 135 | DrawingArea: gtk.NewDrawingArea(), 136 | R: r, 137 | } 138 | w.left = r.GetParameter("left") 139 | w.top = r.GetParameter("top") 140 | w.right = r.GetParameter("right") 141 | w.bottom = r.GetParameter("bottom") 142 | w.width = r.GetParameter("width") 143 | w.height = r.GetParameter("height") 144 | w.zoom = r.GetParameter("zoom") 145 | w.mouseX = r.GetParameter("mouseX") 146 | w.mouseY = r.GetParameter("mouseY") 147 | w.options = r.GetParameter("options") 148 | w.page = r.GetParameter("page") 149 | w.zoomRate = r.GetParameter("zoomRate") 150 | w.leftIsFloat64 = w.left.GetType() == "float64" 151 | w.zoomIsFloat64 = w.zoom.GetType() == "float64" 152 | w.Connect("expose-event", w.Draw) 153 | w.Connect("configure-event", w.Configure) 154 | w.Connect("motion-notify-event", func(ctx *glib.CallbackContext) { 155 | arg := ctx.Args(0) 156 | mev := *(**gdk.EventMotion)(unsafe.Pointer(&arg)) 157 | w.OnMotion(mev) 158 | }) 159 | w.Connect("button-press-event", func(ctx *glib.CallbackContext) { 160 | arg := ctx.Args(0) 161 | mev := *(**gdk.EventButton)(unsafe.Pointer(&arg)) 162 | w.OnButton(mev) 163 | }) 164 | w.Connect("button-release-event", func(ctx *glib.CallbackContext) { 165 | arg := ctx.Args(0) 166 | mev := *(**gdk.EventButton)(unsafe.Pointer(&arg)) 167 | w.OnButton(mev) 168 | }) 169 | w.Connect("scroll-event", func(ctx *glib.CallbackContext) { 170 | arg := ctx.Args(0) 171 | sev := *(**gdk.EventScroll)(unsafe.Pointer(&arg)) 172 | w.OnScroll(sev) 173 | }) 174 | w.Connect("key-press-event", func(ctx *glib.CallbackContext) { 175 | arg := ctx.Args(0) 176 | kev := *(**gdk.EventKey)(unsafe.Pointer(&arg)) 177 | w.OnKeyPress(kev) 178 | }) 179 | w.R.SetRequestPaintFunc(func() { 180 | //w.UpdateParamWidgets() 181 | w.needsUpdate = true 182 | w.needsPaint = true 183 | //w.QueueDraw() 184 | //w.GetWindow().Invalidate(nil, false) 185 | }) 186 | w.SetCanFocus(true) 187 | // w.SetFocusOnClick(true) // missing? 188 | w.SetEvents(int(gdk.POINTER_MOTION_MASK | gdk.BUTTON_PRESS_MASK | gdk.BUTTON_RELEASE_MASK | gdk.EXPOSURE_MASK | gdk.SCROLL_MASK | gdk.KEY_PRESS_MASK)) 189 | // This doesn't seem to actually work? 190 | glib.TimeoutAdd(3000, func() int { 191 | if w.needsPaint { 192 | w.QueueDraw() 193 | } 194 | return 1 195 | }) 196 | 197 | return w 198 | } 199 | 200 | func (w *GtkRenderWidget) SetNeedsPaint() { 201 | w.needsPaint = true 202 | w.QueueDraw() 203 | } 204 | 205 | func (w *GtkRenderWidget) UpdateParamWidgets() { 206 | w.R.Lock() 207 | defer w.R.Unlock() 208 | for i := 0; i < len(w.ParamWidgets); i++ { 209 | w.ParamWidgets[i].Update() 210 | } 211 | w.needsUpdate = false 212 | } 213 | 214 | func (w *GtkRenderWidget) Configure() { 215 | //fmt.Printf("Configure called.\n") 216 | if w.pixbuf != nil { 217 | w.pixbuf.Unref() 218 | } 219 | allocation := w.GetAllocation() 220 | w.width.SetValueInt(allocation.Width) 221 | w.height.SetValueInt(allocation.Height) 222 | 223 | w.pixbuf = gdkpixbuf.NewPixbuf(gdkpixbuf.GDK_COLORSPACE_RGB, true, 8, allocation.Width, allocation.Height) 224 | w.needsPaint = true 225 | w.needsUpdate = true 226 | } 227 | 228 | func (w *GtkRenderWidget) Draw(ctx *glib.CallbackContext) { 229 | if w.needsUpdate { 230 | w.UpdateParamWidgets() 231 | } 232 | if w.needsPaint || w.Image == nil { 233 | img := w.R.Render() 234 | if img == nil { 235 | return 236 | } 237 | switch a := img.(type) { 238 | case *image.RGBA: 239 | w.Image = a 240 | w.needsPaint = false 241 | case *image.NRGBA: 242 | w.Image = a 243 | w.needsPaint = false 244 | default: 245 | i2 := image.NewRGBA(img.Bounds()) 246 | draw.Draw(i2, img.Bounds(), img, image.ZP, draw.Src) 247 | w.Image = i2 248 | w.needsPaint = false 249 | } 250 | } 251 | // copy from Go image to GDK image 252 | w.pixbuf.Fill(0) 253 | switch a := w.Image.(type) { 254 | case *image.RGBA: 255 | GdkPixelCopy(a, w.pixbuf, image.ZR, image.ZP) 256 | case *image.NRGBA: 257 | GdkPixelCopyNRGBA(a, w.pixbuf, image.ZR, image.ZP) 258 | } 259 | 260 | // Draw GDK image on window 261 | win := w.GetWindow() 262 | draw := win.GetDrawable() 263 | gc := gdk.NewGC(draw) 264 | draw.DrawPixbuf(gc, w.pixbuf, 0, 0, 0, 0, w.pixbuf.GetWidth(), w.pixbuf.GetHeight(), gdk.RGB_DITHER_NONE, 0, 0) 265 | } 266 | 267 | func GdkPixelCopy(source *image.RGBA, target *gdkpixbuf.Pixbuf, region image.Rectangle, targetOffset image.Point) { 268 | if source == nil { 269 | return 270 | } 271 | if target == nil { 272 | return 273 | } 274 | sourceBounds := source.Rect 275 | targetBounds := image.Rect(0, 0, target.GetWidth(), target.GetHeight()) 276 | sourceRowstride := source.Stride 277 | targetRowstride := target.GetRowstride() 278 | //Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*4] 279 | var targetPix []byte 280 | var sourcePix []uint8 281 | targetPix = target.GetPixelsWithLength() 282 | sourcePix = source.Pix 283 | 284 | if region.Dx() == 0 { 285 | region = sourceBounds.Intersect(targetBounds.Sub(targetOffset)) 286 | } 287 | region = region.Intersect(sourceBounds).Intersect(targetBounds.Sub(targetOffset)) 288 | var x, y int 289 | 290 | for y = region.Min.Y; y < region.Max.Y; y++ { 291 | for x = region.Min.X; x < region.Max.X; x++ { 292 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4] = sourcePix[y*sourceRowstride+x*4] 293 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+1] = sourcePix[y*sourceRowstride+x*4+1] 294 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+2] = sourcePix[y*sourceRowstride+x*4+2] 295 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+3] = sourcePix[y*sourceRowstride+x*4+3] 296 | } 297 | } 298 | } 299 | 300 | func GdkPixelCopyNRGBA(source *image.NRGBA, target *gdkpixbuf.Pixbuf, region image.Rectangle, targetOffset image.Point) { 301 | if source == nil { 302 | return 303 | } 304 | if target == nil { 305 | return 306 | } 307 | sourceBounds := source.Rect 308 | targetBounds := image.Rect(0, 0, target.GetWidth(), target.GetHeight()) 309 | sourceRowstride := source.Stride 310 | targetRowstride := target.GetRowstride() 311 | //Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*4] 312 | var targetPix []byte 313 | var sourcePix []uint8 314 | targetPix = target.GetPixelsWithLength() 315 | sourcePix = source.Pix 316 | 317 | if region.Dx() == 0 { 318 | region = sourceBounds.Intersect(targetBounds.Sub(targetOffset)) 319 | } 320 | region = region.Intersect(sourceBounds).Intersect(targetBounds.Sub(targetOffset)) 321 | var x, y int 322 | 323 | for y = region.Min.Y; y < region.Max.Y; y++ { 324 | for x = region.Min.X; x < region.Max.X; x++ { 325 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4] = sourcePix[y*sourceRowstride+x*4] 326 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+1] = sourcePix[y*sourceRowstride+x*4+1] 327 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+2] = sourcePix[y*sourceRowstride+x*4+2] 328 | targetPix[(y+targetOffset.Y)*targetRowstride+(x+targetOffset.X)*4+3] = sourcePix[y*sourceRowstride+x*4+3] 329 | } 330 | } 331 | } 332 | 333 | func (w *GtkRenderWidget) OnScroll(e *gdk.EventScroll) { 334 | // the case of SCROLL_Down is incorrect in gdk.go 335 | // todo: fix this when it is fixed upstream 336 | // e.Direction always has same value, and does not match documentation 337 | if gdk.ModifierType(e.State) > (1 << 30) { 338 | if w.zoomIsFloat64 { 339 | w.zoom.SetValueFloat64(w.zoom.GetValueFloat64() - 1) 340 | } else { 341 | w.zoom.SetValueInt(w.zoom.GetValueInt() - 1) 342 | } 343 | if w.options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || w.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 344 | mult := 1 + rv.ZOOM_RATE 345 | if w.zoomRate.GetValueFloat64() > 0 { 346 | mult = 1 + w.zoomRate.GetValueFloat64() 347 | } 348 | if w.leftIsFloat64 { 349 | 350 | zwidth := w.right.GetValueFloat64() - w.left.GetValueFloat64() 351 | zheight := w.bottom.GetValueFloat64() - w.top.GetValueFloat64() 352 | nzwidth := zwidth * mult 353 | nzheight := zheight * mult 354 | cx := float64(e.X) / float64(w.width.GetValueInt()) 355 | cy := float64(e.Y) / float64(w.height.GetValueInt()) 356 | if w.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 357 | cx = 0.5 358 | cy = 0.5 359 | } 360 | //fmt.Printf("zoomOut: mult: %v zwidth: %v nzwidth: %v cx: %v left: %v nleft: %v\n", mult, zwidth, nzwidth, cx, left.GetValueFloat64(), left.GetValueFloat64()-((nzwidth-zwidth)*cx)) 361 | w.left.SetValueFloat64(w.left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 362 | w.top.SetValueFloat64(w.top.GetValueFloat64() - ((nzheight - zheight) * cy)) 363 | w.right.SetValueFloat64(w.left.GetValueFloat64() + nzwidth) 364 | w.bottom.SetValueFloat64(w.top.GetValueFloat64() + nzheight) 365 | } 366 | } 367 | 368 | //if gdk.ScrollDirection(e.Direction) == gdk.SCROLL_UP { 369 | } else { 370 | if w.zoomIsFloat64 { 371 | w.zoom.SetValueFloat64(w.zoom.GetValueFloat64() + 1) 372 | } else { 373 | w.zoom.SetValueInt(w.zoom.GetValueInt() + 1) 374 | } 375 | if w.options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || w.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 376 | mult := 1 - rv.ZOOM_RATE 377 | if w.zoomRate.GetValueFloat64() > 0 { 378 | mult = 1 - w.zoomRate.GetValueFloat64() 379 | } 380 | if w.leftIsFloat64 { 381 | zwidth := w.right.GetValueFloat64() - w.left.GetValueFloat64() 382 | zheight := w.bottom.GetValueFloat64() - w.top.GetValueFloat64() 383 | nzwidth := zwidth * mult 384 | nzheight := zheight * mult 385 | cx := float64(e.X) / float64(w.width.GetValueInt()) 386 | cy := float64(e.Y) / float64(w.height.GetValueInt()) 387 | if w.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 388 | cx = 0.5 389 | cy = 0.5 390 | } 391 | w.left.SetValueFloat64(w.left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 392 | w.top.SetValueFloat64(w.top.GetValueFloat64() - ((nzheight - zheight) * cy)) 393 | w.right.SetValueFloat64(w.left.GetValueFloat64() + nzwidth) 394 | w.bottom.SetValueFloat64(w.top.GetValueFloat64() + nzheight) 395 | } 396 | } 397 | } 398 | w.needsUpdate = true 399 | w.SetNeedsPaint() 400 | 401 | } 402 | 403 | func (w *GtkRenderWidget) OnMotion(e *gdk.EventMotion) { 404 | // fmt.Printf("Motion: X:%v Y:%v sx:%v sy:%v mouseIsDown:%v dragging:%v\n", e.X, e.Y, w.sx, w.sy, w.mouseIsDown, w.dragging) 405 | w.mouseX.SetValueFloat64(float64(e.X)) 406 | w.mouseY.SetValueFloat64(float64(e.Y)) 407 | 408 | if w.needsPaint { 409 | w.QueueDraw() 410 | } 411 | 412 | if w.mouseIsDown && w.dragging == false { 413 | if ((e.X - w.sx) > 3) || ((w.sx - e.X) > 3) || ((e.Y - w.sy) > 3) || ((w.sy - e.Y) > 3) { 414 | w.dragging = true 415 | } 416 | } 417 | 418 | if w.dragging { 419 | if w.leftIsFloat64 { 420 | width := w.right.GetValueFloat64() - w.left.GetValueFloat64() 421 | height := w.bottom.GetValueFloat64() - w.top.GetValueFloat64() 422 | dx := width / float64(w.width.GetValueInt()) 423 | dy := height / float64(w.height.GetValueInt()) 424 | cx := float64(e.X-w.sx) * dx 425 | cy := float64(e.Y-w.sy) * dy 426 | // fmt.Printf("dx %v dy %v cx %v cy %v\n", dx, dy, cx, cy) 427 | w.left.SetValueFloat64(w.left.GetValueFloat64() - cx) 428 | w.right.SetValueFloat64(w.right.GetValueFloat64() - cx) 429 | w.top.SetValueFloat64(w.top.GetValueFloat64() - cy) 430 | w.bottom.SetValueFloat64(w.bottom.GetValueFloat64() - cy) 431 | } else { 432 | width := w.right.GetValueInt() - w.left.GetValueInt() 433 | height := w.bottom.GetValueInt() - w.top.GetValueInt() 434 | dx := float64(width) / float64(w.width.GetValueInt()) 435 | dy := float64(height) / float64(w.height.GetValueInt()) 436 | cx := float64(e.X-w.sx) * dx 437 | cy := float64(e.Y-w.sy) * dy 438 | w.left.SetValueInt(int(float64(w.left.GetValueInt()) - cx)) 439 | w.right.SetValueInt(int(float64(w.right.GetValueInt()) - cx)) 440 | w.top.SetValueInt(int(float64(w.top.GetValueInt()) - cy)) 441 | w.bottom.SetValueInt(int(float64(w.bottom.GetValueInt()) - cy)) 442 | } 443 | // Draw(r.Render(), buf.RGBA()) 444 | w.needsUpdate = true 445 | w.SetNeedsPaint() 446 | 447 | w.sx = e.X 448 | w.sy = e.Y 449 | 450 | } 451 | 452 | } 453 | 454 | func (w *GtkRenderWidget) OnButton(e *gdk.EventButton) { 455 | // fmt.Printf("Button called with %v\n", e) 456 | w.mouseX.SetValueFloat64(float64(e.X)) 457 | w.mouseY.SetValueFloat64(float64(e.Y)) 458 | w.GrabFocus() 459 | 460 | if gdk.EventType(e.Type) == gdk.BUTTON_PRESS { 461 | if w.dragging == false && w.mouseIsDown == false && e.Button == 1 { 462 | // fmt.Println("Mousedown") 463 | w.sx = e.X 464 | w.sy = e.Y 465 | w.mouseIsDown = true 466 | } 467 | } 468 | if gdk.EventType(e.Type) == gdk.BUTTON_RELEASE { 469 | // fmt.Println("Mouseup") 470 | w.mouseIsDown = false 471 | w.dragging = false 472 | } 473 | } 474 | 475 | func (w *GtkRenderWidget) OnKeyPress(e *gdk.EventKey) { 476 | if e.Keyval == gdk.KEY_Page_Up { 477 | w.page.SetValueInt(w.page.GetValueInt() - 1) 478 | w.needsUpdate = true 479 | w.SetNeedsPaint() 480 | } 481 | if e.Keyval == gdk.KEY_Page_Down { 482 | w.page.SetValueInt(w.page.GetValueInt() + 1) 483 | w.needsUpdate = true 484 | w.SetNeedsPaint() 485 | } 486 | } 487 | 488 | type GtkParamWidget struct { 489 | gtk.IWidget 490 | P rv.RenderParameter 491 | } 492 | 493 | func NewGtkParamWidget(p rv.RenderParameter, w *GtkRenderWidget) *GtkParamWidget { 494 | tv := gtk.NewTextView() 495 | r := &GtkParamWidget{ 496 | IWidget: tv, 497 | P: p, 498 | } 499 | tv.SetEditable(true) 500 | tv.SetCursorVisible(true) 501 | tb := tv.GetBuffer() 502 | tb.SetText(rv.GetParameterValueAsString(p)) 503 | tb.Connect("changed", func() { 504 | var start, end gtk.TextIter 505 | tb.GetBounds(&start, &end) 506 | s := tb.GetText(&start, &end, false) 507 | pValue := rv.GetParameterValueAsString(r.P) 508 | if s != pValue { 509 | rv.SetParameterValueFromString(r.P, s) 510 | } 511 | w.SetNeedsPaint() 512 | }) 513 | return r 514 | } 515 | 516 | func (w *GtkParamWidget) Update() { 517 | switch a := w.IWidget.(type) { 518 | case *gtk.TextView: 519 | tb := a.GetBuffer() 520 | tb.SetText(rv.GetParameterValueAsString(w.P)) 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /driver/shiny/view.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build android shiny 6 | 7 | package shiny 8 | 9 | import ( 10 | "image" 11 | "image/color" 12 | "image/draw" 13 | "log" 14 | 15 | rv "github.com/TheGrum/renderview" 16 | 17 | "golang.org/x/exp/shiny/widget/theme" 18 | 19 | "golang.org/x/exp/shiny/driver" 20 | "golang.org/x/exp/shiny/screen" 21 | "golang.org/x/exp/shiny/unit" 22 | "golang.org/x/exp/shiny/widget" 23 | "golang.org/x/exp/shiny/widget/node" 24 | "golang.org/x/mobile/event/key" 25 | "golang.org/x/mobile/event/lifecycle" 26 | "golang.org/x/mobile/event/mouse" 27 | "golang.org/x/mobile/event/paint" 28 | "golang.org/x/mobile/event/size" 29 | ) 30 | 31 | // FrameBuffer sets up a Shiny screen and runs a mainloop rendering the rv.RenderModel 32 | func FrameBuffer(m rv.RenderModel) { 33 | driver.Main(GetMainLoop(m)) 34 | } 35 | 36 | // Main sets up a Shiny screen and runs a mainloop rendering the rv.RenderModel; with widgets 37 | // when widgets are functional in Shiny 38 | func Main(m rv.RenderModel) { 39 | //driver.Main(GetMainLoopWithWidgets(m)) 40 | driver.Main(GetMainLoop(m)) 41 | } 42 | 43 | func GetMainLoop(r rv.RenderModel) func(s screen.Screen) { 44 | return func(s screen.Screen) { 45 | MainLoop(s, r) 46 | } 47 | } 48 | 49 | func MainLoop(s screen.Screen, r rv.RenderModel) { 50 | var needsPaint = false 51 | var mouseIsDown = false 52 | var dragging bool = false 53 | var dx, dy float64 54 | var sx, sy float32 55 | 56 | var left, top, right, bottom, zoom rv.RenderParameter 57 | left = r.GetParameter("left") 58 | top = r.GetParameter("top") 59 | right = r.GetParameter("right") 60 | bottom = r.GetParameter("bottom") 61 | zoom = r.GetParameter("zoom") 62 | mouseX := r.GetParameter("mouseX") 63 | mouseY := r.GetParameter("mouseY") 64 | options := r.GetParameter("options") 65 | page := r.GetParameter("page") 66 | // offsetX := r.GetParameter("offsetX") 67 | // offsetY := r.GetParameter("offsetY") 68 | 69 | leftIsFloat64 := left.GetType() == "float64" 70 | zoomIsFloat64 := zoom.GetType() == "float64" 71 | 72 | w, err := s.NewWindow(nil) 73 | if err != nil { 74 | handleError(err) 75 | return 76 | } 77 | 78 | buf := screen.Buffer(nil) 79 | defer func() { 80 | if buf != nil { 81 | buf.Release() 82 | } 83 | w.Release() 84 | }() 85 | 86 | r.SetRequestPaintFunc(func() { 87 | if buf == nil || r == nil || w == nil { 88 | return 89 | } 90 | needsPaint = true 91 | w.Send(paint.Event{}) 92 | }) 93 | 94 | for { 95 | switch e := w.NextEvent().(type) { 96 | case lifecycle.Event: 97 | if e.To == lifecycle.StageDead { 98 | return 99 | } 100 | 101 | case paint.Event: 102 | w.Upload(image.Point{}, buf, buf.Bounds()) 103 | w.Publish() 104 | 105 | case key.Event: 106 | if e.Code == key.CodeEscape { 107 | return 108 | } 109 | if e.Code == key.CodePageUp && e.Direction == key.DirPress { 110 | page.SetValueInt(page.GetValueInt() - 1) 111 | needsPaint = true 112 | } 113 | if e.Code == key.CodePageDown && e.Direction == key.DirPress { 114 | page.SetValueInt(page.GetValueInt() + 1) 115 | needsPaint = true 116 | } 117 | 118 | case mouse.Event: 119 | //fmt.Printf("mouse pos(%v)\n", e) 120 | mouseX.SetValueFloat64(float64(e.X)) 121 | mouseY.SetValueFloat64(float64(e.Y)) 122 | 123 | if dragging == false && e.Direction == mouse.DirPress && e.Button == mouse.ButtonLeft { 124 | // fmt.Printf("mouse down left(%v)\n", e) 125 | sx = e.X 126 | sy = e.Y 127 | mouseIsDown = true 128 | } 129 | if e.Button == mouse.ButtonWheelDown { 130 | if zoomIsFloat64 { 131 | zoom.SetValueFloat64(zoom.GetValueFloat64() - 1) 132 | } else { 133 | zoom.SetValueInt(zoom.GetValueInt() - 1) 134 | } 135 | if options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 136 | mult := 1 + rv.ZOOM_RATE 137 | if leftIsFloat64 { 138 | 139 | zwidth := right.GetValueFloat64() - left.GetValueFloat64() 140 | zheight := bottom.GetValueFloat64() - top.GetValueFloat64() 141 | nzwidth := zwidth * mult 142 | nzheight := zheight * mult 143 | cx := float64(e.X) / float64(buf.Size().X) 144 | cy := float64(e.Y) / float64(buf.Size().Y) 145 | if options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 146 | cx = 0.5 147 | cy = 0.5 148 | } 149 | //fmt.Printf("zoomOut: mult: %v zwidth: %v nzwidth: %v cx: %v left: %v nleft: %v\n", mult, zwidth, nzwidth, cx, left.GetValueFloat64(), left.GetValueFloat64()-((nzwidth-zwidth)*cx)) 150 | left.SetValueFloat64(left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 151 | top.SetValueFloat64(top.GetValueFloat64() - ((nzheight - zheight) * cy)) 152 | right.SetValueFloat64(left.GetValueFloat64() + nzwidth) 153 | bottom.SetValueFloat64(top.GetValueFloat64() + nzheight) 154 | needsPaint = true 155 | 156 | } 157 | } 158 | 159 | } 160 | if e.Button == mouse.ButtonWheelUp { 161 | if zoomIsFloat64 { 162 | zoom.SetValueFloat64(zoom.GetValueFloat64() + 1) 163 | } else { 164 | zoom.SetValueInt(zoom.GetValueInt() + 1) 165 | } 166 | if options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 167 | mult := 1 - rv.ZOOM_RATE 168 | if leftIsFloat64 { 169 | zwidth := right.GetValueFloat64() - left.GetValueFloat64() 170 | zheight := bottom.GetValueFloat64() - top.GetValueFloat64() 171 | nzwidth := zwidth * mult 172 | nzheight := zheight * mult 173 | cx := float64(e.X) / float64(buf.Size().X) 174 | cy := float64(e.Y) / float64(buf.Size().Y) 175 | if options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 176 | cx = 0.5 177 | cy = 0.5 178 | } 179 | left.SetValueFloat64(left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 180 | top.SetValueFloat64(top.GetValueFloat64() - ((nzheight - zheight) * cy)) 181 | right.SetValueFloat64(left.GetValueFloat64() + nzwidth) 182 | bottom.SetValueFloat64(top.GetValueFloat64() + nzheight) 183 | needsPaint = true 184 | } 185 | } 186 | } 187 | if e.Direction == mouse.DirNone && mouseIsDown { 188 | // fmt.Printf("mouse drag(%v) dragging (%v)\n", e, dragging) 189 | if dragging == false { 190 | // fmt.Printf("Checking %v, %v, %v, %v\n", e.X, e.Y, sx, sy) 191 | if ((e.X - sx) > 3) || ((sx - e.X) > 3) || ((e.Y - sy) > 3) || ((sy - e.Y) > 3) { 192 | dragging = true 193 | // fmt.Printf("Dragging.\n") 194 | } 195 | } else { 196 | if leftIsFloat64 { 197 | width := right.GetValueFloat64() - left.GetValueFloat64() 198 | height := bottom.GetValueFloat64() - top.GetValueFloat64() 199 | dx = width / float64(buf.Size().X) 200 | dy = height / float64(buf.Size().Y) 201 | cx := float64(e.X-sx) * dx 202 | cy := float64(e.Y-sy) * dy 203 | // fmt.Printf("dx %v dy %v cx %v cy %v\n", dx, dy, cx, cy) 204 | left.SetValueFloat64(left.GetValueFloat64() - cx) 205 | right.SetValueFloat64(right.GetValueFloat64() - cx) 206 | top.SetValueFloat64(top.GetValueFloat64() - cy) 207 | bottom.SetValueFloat64(bottom.GetValueFloat64() - cy) 208 | } else { 209 | width := right.GetValueInt() - left.GetValueInt() 210 | height := bottom.GetValueInt() - top.GetValueInt() 211 | dx = float64(width) / float64(buf.Size().X) 212 | dy = float64(height) / float64(buf.Size().Y) 213 | cx := float64(e.X-sx) * dx 214 | cy := float64(e.Y-sy) * dy 215 | left.SetValueInt(int(float64(left.GetValueInt()) - cx)) 216 | right.SetValueInt(int(float64(right.GetValueInt()) - cx)) 217 | top.SetValueInt(int(float64(top.GetValueInt()) - cy)) 218 | bottom.SetValueInt(int(float64(bottom.GetValueInt()) - cy)) 219 | } 220 | Draw(r.Render(), buf.RGBA()) 221 | 222 | sx = e.X 223 | sy = e.Y 224 | 225 | } 226 | } 227 | if e.Direction == mouse.DirRelease { 228 | dragging = false 229 | mouseIsDown = false 230 | } 231 | 232 | case size.Event: 233 | if buf != nil { 234 | buf.Release() 235 | } 236 | r.GetParameter("width").SetValueInt(e.Size().X) 237 | r.GetParameter("height").SetValueInt(e.Size().Y) 238 | buf, err = s.NewBuffer(e.Size()) 239 | if err != nil { 240 | log.Fatal(err) 241 | } 242 | Draw(r.Render(), buf.RGBA()) 243 | default: 244 | 245 | } 246 | if needsPaint { 247 | needsPaint = false 248 | Draw(r.Render(), buf.RGBA()) 249 | w.Send(paint.Event{}) 250 | } 251 | } 252 | } 253 | 254 | func Draw(mimg image.Image, bimg *image.RGBA) { 255 | if !(mimg == nil) && !(bimg == nil) { 256 | r := mimg.Bounds() 257 | r2 := bimg.Bounds() 258 | mx := r.Dx() 259 | my := r.Dy() 260 | if r2.Dx() < mx { 261 | mx = r2.Dx() 262 | } 263 | if r2.Dy() < my { 264 | my = r2.Dy() 265 | } 266 | r3 := image.Rect(0, 0, mx, my) 267 | draw.Draw(bimg, r3, mimg, image.ZP, draw.Src) 268 | } 269 | } 270 | 271 | func handleError(e error) { 272 | log.Fatal(e) 273 | } 274 | 275 | func GetWidgetMainLoop(r rv.RenderModel) func(s screen.Screen) { 276 | return func(s screen.Screen) { 277 | WidgetMainLoop(s, r) 278 | } 279 | } 280 | 281 | func expand(n node.Node, expandAlongWeight int) node.Node { 282 | return widget.WithLayoutData(n, widget.FlowLayoutData{ 283 | ExpandAcross: true, 284 | AlongWeight: expandAlongWeight, 285 | }) 286 | } 287 | 288 | type RenderWidget struct { 289 | node.LeafEmbed 290 | index int 291 | r rv.RenderModel 292 | 293 | left, 294 | top, 295 | right, 296 | bottom, 297 | width, 298 | height, 299 | zoom, 300 | mouseX, 301 | mouseY, 302 | options, 303 | page rv.RenderParameter 304 | 305 | sx, 306 | sy float32 307 | 308 | leftIsFloat64, 309 | zoomIsFloat64, 310 | mouseIsDown, 311 | dragging bool 312 | } 313 | 314 | func NewRenderWidget(r rv.RenderModel) *RenderWidget { 315 | w := &RenderWidget{ 316 | r: r, 317 | } 318 | w.Wrapper = w 319 | w.left = r.GetParameter("left") 320 | w.top = r.GetParameter("top") 321 | w.right = r.GetParameter("right") 322 | w.bottom = r.GetParameter("bottom") 323 | w.width = r.GetParameter("width") 324 | w.height = r.GetParameter("height") 325 | w.zoom = r.GetParameter("zoom") 326 | w.mouseX = r.GetParameter("mouseX") 327 | w.mouseY = r.GetParameter("mouseY") 328 | w.options = r.GetParameter("options") 329 | w.page = r.GetParameter("page") 330 | w.leftIsFloat64 = w.left.GetType() == "float64" 331 | w.zoomIsFloat64 = w.zoom.GetType() == "float64" 332 | r.SetRequestPaintFunc(func() { 333 | w.Mark(node.MarkNeedsPaintBase) 334 | }) 335 | return w 336 | } 337 | 338 | func (m *RenderWidget) OnInputEvent(e interface{}, origin image.Point) node.EventHandled { 339 | switch e := e.(type) { 340 | case key.Event: 341 | if e.Code == key.CodeEscape { 342 | return node.NotHandled 343 | } 344 | if e.Code == key.CodePageUp && e.Direction == key.DirPress { 345 | m.page.SetValueInt(m.page.GetValueInt() - 1) 346 | m.Mark(node.MarkNeedsPaintBase) 347 | } 348 | if e.Code == key.CodePageDown && e.Direction == key.DirPress { 349 | m.page.SetValueInt(m.page.GetValueInt() + 1) 350 | m.Mark(node.MarkNeedsPaintBase) 351 | } 352 | 353 | case mouse.Event: 354 | //fmt.Printf("mouse pos(%v)\n", e) 355 | m.mouseX.SetValueFloat64(float64(e.X)) 356 | m.mouseY.SetValueFloat64(float64(e.Y)) 357 | 358 | if m.dragging == false && e.Direction == mouse.DirPress && e.Button == mouse.ButtonLeft { 359 | // fmt.Printf("mouse down left(%v)\n", e) 360 | m.sx = e.X 361 | m.sy = e.Y 362 | m.mouseIsDown = true 363 | } 364 | if e.Button == mouse.ButtonWheelDown { 365 | if m.zoomIsFloat64 { 366 | m.zoom.SetValueFloat64(m.zoom.GetValueFloat64() - 1) 367 | } else { 368 | m.zoom.SetValueInt(m.zoom.GetValueInt() - 1) 369 | } 370 | if m.options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || m.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 371 | mult := 1 + rv.ZOOM_RATE 372 | if m.leftIsFloat64 { 373 | 374 | zwidth := m.right.GetValueFloat64() - m.left.GetValueFloat64() 375 | zheight := m.bottom.GetValueFloat64() - m.top.GetValueFloat64() 376 | nzwidth := zwidth * mult 377 | nzheight := zheight * mult 378 | cx := float64(e.X) / float64(m.width.GetValueInt()) 379 | cy := float64(e.Y) / float64(m.height.GetValueInt()) 380 | if m.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 381 | cx = 0.5 382 | cy = 0.5 383 | } 384 | //fmt.Printf("zoomOut: mult: %v zwidth: %v nzwidth: %v cx: %v left: %v nleft: %v\n", mult, zwidth, nzwidth, cx, left.GetValueFloat64(), left.GetValueFloat64()-((nzwidth-zwidth)*cx)) 385 | m.left.SetValueFloat64(m.left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 386 | m.top.SetValueFloat64(m.top.GetValueFloat64() - ((nzheight - zheight) * cy)) 387 | m.right.SetValueFloat64(m.left.GetValueFloat64() + nzwidth) 388 | m.bottom.SetValueFloat64(m.top.GetValueFloat64() + nzheight) 389 | 390 | m.Mark(node.MarkNeedsPaintBase) 391 | 392 | } 393 | } 394 | 395 | } 396 | if e.Button == mouse.ButtonWheelUp { 397 | if m.zoomIsFloat64 { 398 | m.zoom.SetValueFloat64(m.zoom.GetValueFloat64() + 1) 399 | } else { 400 | m.zoom.SetValueInt(m.zoom.GetValueInt() + 1) 401 | } 402 | if m.options.GetValueInt()&rv.OPT_AUTO_ZOOM == rv.OPT_AUTO_ZOOM || m.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 403 | mult := 1 - rv.ZOOM_RATE 404 | if m.leftIsFloat64 { 405 | zwidth := m.right.GetValueFloat64() - m.left.GetValueFloat64() 406 | zheight := m.bottom.GetValueFloat64() - m.top.GetValueFloat64() 407 | nzwidth := zwidth * mult 408 | nzheight := zheight * mult 409 | cx := float64(e.X) / float64(m.width.GetValueInt()) 410 | 411 | cy := float64(e.Y) / float64(m.height.GetValueInt()) 412 | if m.options.GetValueInt()&rv.OPT_CENTER_ZOOM == rv.OPT_CENTER_ZOOM { 413 | cx = 0.5 414 | cy = 0.5 415 | } 416 | m.left.SetValueFloat64(m.left.GetValueFloat64() - ((nzwidth - zwidth) * cx)) 417 | m.top.SetValueFloat64(m.top.GetValueFloat64() - ((nzheight - zheight) * cy)) 418 | m.right.SetValueFloat64(m.left.GetValueFloat64() + nzwidth) 419 | m.bottom.SetValueFloat64(m.top.GetValueFloat64() + nzheight) 420 | m.Mark(node.MarkNeedsPaintBase) 421 | } 422 | } 423 | } 424 | if e.Direction == mouse.DirNone && m.mouseIsDown { 425 | // fmt.Printf("mouse drag(%v) dragging (%v)\n", e, dragging) 426 | if m.dragging == false { 427 | // fmt.Printf("Checking %v, %v, %v, %v\n", e.X, e.Y, sx, sy) 428 | if ((e.X - m.sx) > 3) || ((m.sx - e.X) > 3) || ((e.Y - m.sy) > 3) || ((m.sy - e.Y) > 3) { 429 | m.dragging = true 430 | // fmt.Printf("Dragging.\n") 431 | } 432 | } else { 433 | if m.leftIsFloat64 { 434 | width := m.right.GetValueFloat64() - m.left.GetValueFloat64() 435 | height := m.bottom.GetValueFloat64() - m.top.GetValueFloat64() 436 | dx := width / float64(m.width.GetValueInt()) 437 | dy := height / float64(m.height.GetValueInt()) 438 | cx := float64(e.X-m.sx) * dx 439 | cy := float64(e.Y-m.sy) * dy 440 | // fmt.Printf("dx %v dy %v cx %v cy %v\n", dx, dy, cx, cy) 441 | m.left.SetValueFloat64(m.left.GetValueFloat64() - cx) 442 | m.right.SetValueFloat64(m.right.GetValueFloat64() - cx) 443 | m.top.SetValueFloat64(m.top.GetValueFloat64() - cy) 444 | m.bottom.SetValueFloat64(m.bottom.GetValueFloat64() - cy) 445 | } else { 446 | width := m.right.GetValueInt() - m.left.GetValueInt() 447 | height := m.bottom.GetValueInt() - m.top.GetValueInt() 448 | dx := float64(width) / float64(m.Rect.Dx()) 449 | dy := float64(height) / float64(m.Rect.Dy()) 450 | cx := float64(e.X-m.sx) * dx 451 | cy := float64(e.Y-m.sy) * dy 452 | m.left.SetValueInt(int(float64(m.left.GetValueInt()) - cx)) 453 | m.right.SetValueInt(int(float64(m.right.GetValueInt()) - cx)) 454 | m.top.SetValueInt(int(float64(m.top.GetValueInt()) - cy)) 455 | m.bottom.SetValueInt(int(float64(m.bottom.GetValueInt()) - cy)) 456 | } 457 | m.Mark(node.MarkNeedsPaintBase) 458 | // Draw(r.Render(), buf.RGBA()) 459 | 460 | m.sx = e.X 461 | m.sy = e.Y 462 | 463 | } 464 | } 465 | if e.Direction == mouse.DirRelease { 466 | m.dragging = false 467 | m.mouseIsDown = false 468 | } 469 | 470 | } 471 | return node.NotHandled 472 | } 473 | 474 | func (m *RenderWidget) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error { 475 | m.width.SetValueInt(m.Rect.Dx()) 476 | m.height.SetValueInt(m.Rect.Dy()) 477 | m.Marks.UnmarkNeedsPaintBase() 478 | Draw(m.r.Render(), ctx.Dst) 479 | return nil 480 | } 481 | 482 | func WidgetMainLoop(s screen.Screen, r rv.RenderModel) { 483 | w := GetRenderWidgetWithSidebar(r) 484 | if err := widget.RunWindow(s, w, nil); err != nil { 485 | log.Fatal(err) 486 | } 487 | } 488 | 489 | func GetRenderWidgetWithSidebar(r rv.RenderModel) node.Node { 490 | sideflow := widget.NewFlow(widget.AxisVertical) 491 | names := r.GetParameterNames() 492 | 493 | for i := 0; i < len(names); i++ { 494 | sideflow.Insert(widget.NewLabel(names[i]), nil) 495 | //e := widget.NewLabel("test") 496 | //e := editor.NewEditor(inconsolata.Regular8x16, nil) 497 | //e := NewTextEdit(r.GetParameter(names[i]).GetValueString(), inconsolata.Regular8x16, nil) 498 | //e := NewTextEdit("text", inconsolata.Regular8x16, nil) 499 | // e.Text = r.GetParameter(names[i]).GetValueString() 500 | //e.Text = "Test" 501 | //e.Rect = image.Rectangle{image.ZP, image.Point{50, 16}} 502 | // w := expand(e, 0 503 | //w := widget.NewSizer(unit.Pixels(50), unit.Pixels(30), e) 504 | // widget.NewLabel(r.GetParameter(names[i]).GetValueString())) 505 | //sideflow.Insert(w, nil) 506 | } 507 | sidebar := widget.NewUniform(theme.StaticColor(color.RGBA{0xff, 0xff, 0xff, 0xff}), sideflow) 508 | 509 | // sidebar := widget.NewUniform(theme.Neutral, 510 | // widget.NewPadder(widget.AxisBoth, unit.Ems(0.5), 511 | // sideflow, 512 | // ), 513 | // ) 514 | divider := widget.NewSizer(unit.Value{}, unit.DIPs(2), 515 | widget.NewUniform(theme.StaticColor(color.RGBA{0xbf, 0xbf, 0xb0, 0xff}), nil)) 516 | 517 | body := widget.NewUniform(theme.StaticColor(color.RGBA{0xbf, 0xbf, 0xb0, 0xff}), NewRenderWidget(r)) 518 | 519 | w := widget.NewFlow(widget.AxisHorizontal, 520 | expand(widget.NewSheet(sidebar), 0), 521 | expand(widget.NewSheet(divider), 0), 522 | expand(widget.NewSheet(body), 1), 523 | ) 524 | 525 | return w 526 | } 527 | 528 | type ParamEdits struct { 529 | P []ParamEdit 530 | } 531 | 532 | type ParamEdit struct { 533 | R rv.RenderModel 534 | P rv.RenderParameter 535 | N node.Node 536 | } 537 | -------------------------------------------------------------------------------- /examples/animgif/animgif.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build example 6 | 7 | package main 8 | 9 | import ( 10 | "flag" 11 | "image/gif" 12 | "log" 13 | "math" 14 | "os" 15 | "time" 16 | 17 | rv "github.com/TheGrum/renderview" 18 | "github.com/TheGrum/renderview/driver/shiny" 19 | ) 20 | 21 | func main() { 22 | flag.Parse() 23 | 24 | var ( 25 | f *os.File 26 | err error 27 | ) 28 | if flag.NArg() > 0 { 29 | f, err = os.Open(flag.Arg(0)) 30 | handleError(err) 31 | } else { 32 | f, err = os.Open("test.gif") 33 | handleError(err) 34 | } 35 | 36 | images, err := gif.DecodeAll(f) 37 | handleError(err) 38 | f.Close() 39 | 40 | numImages := len(images.Image) 41 | 42 | start := time.Now() 43 | m := rv.NewBasicRenderModel() 44 | m.AddParameters(rv.NewIntRP("page", 0)) 45 | page := m.GetParameter("page") 46 | m.InnerRender = func() { 47 | p := page.GetValueInt() 48 | if p > numImages { 49 | p = numImages 50 | page.SetValueInt(p) 51 | } else if p < 0 { 52 | p = 0 53 | page.SetValueInt(p) 54 | } 55 | if p > 0 { 56 | m.Img = images.Image[p-1] 57 | } else { 58 | d := int(math.Floor(time.Since(start).Seconds()*250)) % numImages 59 | m.Img = images.Image[d] 60 | } 61 | } 62 | go func(m *rv.BasicRenderModel) { 63 | ticker := time.NewTicker(time.Millisecond * 250) 64 | for _ = range ticker.C { 65 | m.RequestPaint() 66 | } 67 | }(m) 68 | shiny.FrameBuffer(m) 69 | } 70 | 71 | func handleError(err error) { 72 | if !(err == nil) { 73 | log.Fatal(err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/lsystem/demo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build example 6 | // marked as an example to not build automatically, 7 | // go build -tags 'example' 8 | // in this directory to build 9 | 10 | package main 11 | 12 | import ( 13 | rv "github.com/TheGrum/renderview" 14 | "github.com/TheGrum/renderview/driver" 15 | ) 16 | 17 | func main() { 18 | m := rv.NewBasicRenderModel() 19 | m.AddParameters( 20 | rv.SetHints(rv.HINT_SIDEBAR, 21 | rv.NewFloat64RP("left", -30), 22 | rv.NewFloat64RP("top", -30), 23 | rv.NewFloat64RP("right", 30), 24 | rv.NewFloat64RP("bottom", 30), 25 | rv.NewIntRP("width", 100), 26 | rv.NewIntRP("height", 100), 27 | rv.NewIntRP("options", rv.OPT_AUTO_ZOOM))...) 28 | m.AddParameters( 29 | rv.SetHints(rv.HINT_HIDE, 30 | rv.NewStringRP("LSystemResult", ""), 31 | )...) 32 | lsystemRP := rv.NewStringRP("lsystem", "FX\nX=X+YF+\nY=-FX-Y\n") 33 | m.AddParameters(rv.SetHints(rv.HINT_FULLTEXT, lsystemRP)...) 34 | m.AddParameters( 35 | rv.SetHints(rv.HINT_FOOTER, 36 | rv.NewFloat64RP("angle", 90), 37 | rv.NewIntRP("depth", 5))...) 38 | c := rv.NewChangeMonitor() 39 | c.AddParameters(m.Params[8], m.Params[10]) // lsystem, depth 40 | m.InnerRender = func() { 41 | m.Img = RenderLSystemModel(m, c) 42 | m.NeedsRender = true 43 | } 44 | m.NeedsRender = true 45 | driver.Main(m) 46 | } 47 | -------------------------------------------------------------------------------- /examples/lsystem/lsystem.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build example 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "strings" 12 | ) 13 | 14 | func Calculate(lsystem string, depth int) string { 15 | // Parse the rules 16 | rules := strings.Split(lsystem, "\n") 17 | if len(rules) < 1 { 18 | return "" 19 | } 20 | 21 | // First line is starting state 22 | start := rules[0] 23 | rules = rules[1:] 24 | 25 | if len(rules) < 1 { 26 | // no rules, so state is unchanged 27 | return start 28 | } 29 | 30 | elements := make([]string, len(rules), len(rules)) 31 | elementrules := make([]string, len(rules), len(rules)) 32 | // → \u2192 33 | for i, k := range rules { 34 | k = strings.Replace(k, "\u2192", "=", -1) 35 | parts := strings.Split(k, "=") 36 | elements[i] = parts[0] 37 | if len(parts) > 1 { 38 | elementrules[i] = parts[1] 39 | } else { 40 | elementrules[i] = "" 41 | } 42 | } 43 | 44 | result := start 45 | for i := 0; i < depth; i++ { 46 | result = CalculateSinglePass(result, elements, elementrules) 47 | } 48 | return result 49 | } 50 | 51 | func CalculateSinglePass(state string, elements []string, elementrules []string) string { 52 | buffer := bytes.NewBuffer(make([]byte, 0, len(state)*100)) 53 | 54 | for _, k := range state { 55 | s := string(k) 56 | for i := 0; i < len(elements); i++ { 57 | if s == elements[i] { 58 | buffer.WriteString(elementrules[i]) 59 | s = "" 60 | break 61 | } 62 | } 63 | if s != "" { 64 | buffer.WriteString(s) 65 | } 66 | } 67 | 68 | return buffer.String() 69 | } 70 | -------------------------------------------------------------------------------- /examples/lsystem/lsystemrender.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | // +build example 6 | 7 | package main 8 | 9 | import ( 10 | "image" 11 | "image/color" 12 | "math" 13 | 14 | rv "github.com/TheGrum/renderview" 15 | 16 | "github.com/llgcode/draw2d/draw2dimg" 17 | ) 18 | 19 | const radcon = math.Pi / 180 20 | 21 | type State struct { 22 | Location FPoint 23 | Direction float64 24 | } 25 | 26 | type FPoint struct { 27 | X, Y float64 28 | } 29 | 30 | func (f FPoint) Add(a FPoint) FPoint { 31 | return FPoint{f.X + a.X, f.Y + a.Y} 32 | } 33 | 34 | func (f FPoint) Sub(a FPoint) FPoint { 35 | return FPoint{f.X - a.X, f.Y - a.Y} 36 | } 37 | 38 | func FMin(a float64, b float64) float64 { 39 | if a < b { 40 | return a 41 | } 42 | return b 43 | } 44 | 45 | func FMax(a float64, b float64) float64 { 46 | if a > b { 47 | return a 48 | } 49 | return b 50 | } 51 | 52 | func RenderLSystemModel(m *rv.BasicRenderModel, c *rv.ChangeMonitor) image.Image { 53 | left := m.Params[0].GetValueFloat64() 54 | top := m.Params[1].GetValueFloat64() 55 | right := m.Params[2].GetValueFloat64() 56 | bottom := m.Params[3].GetValueFloat64() 57 | width := m.Params[4].GetValueInt() 58 | height := m.Params[5].GetValueInt() 59 | 60 | lsystem := m.GetParameter("lsystem").GetValueString() 61 | angle := m.GetParameter("angle").GetValueFloat64() 62 | depth := m.GetParameter("depth").GetValueInt() 63 | if depth > 20 { 64 | depth = 20 65 | m.GetParameter("depth").SetValueInt(20) 66 | } 67 | bounds := image.Rect(0, 0, width, height) 68 | result := m.GetParameter("LSystemResult").GetValueString() 69 | magnitude := 1.0 //5 * float64(width) / (right - left) 70 | 71 | if c.HasChanged() { 72 | // lsystem or depth has changed, recalculate 73 | result = Calculate(lsystem, depth) 74 | m.GetParameter("LSystemResult").SetValueString(result) 75 | _, minX, minY, dx, dy := RenderLSystem(left, top, right, bottom, bounds, angle, 1, result) 76 | //fmt.Printf("Applying %v,%v %vx%v mag:%v calmag:%v\n", minX, minY, dx, dy, magnitude, 5*(dx/float64(width))) 77 | //mult := (float64(width) / dx) / 5 78 | left = minX - 1 //* mult 79 | top = minY - 1 //* mult 80 | // mult := magnitude 81 | right = left + dx + 2 //float64(width) //* magnitude) 82 | bottom = top + dy + 2 //*(dx/float64(width)) //* magnitude) 83 | //fmt.Printf("Final %v,%v,%v,%v\n", left, top, right, bottom) 84 | magnitude = 1.0 //5 * float64(width) / (right - left) 85 | 86 | m.Params[0].SetValueFloat64(left) 87 | m.Params[1].SetValueFloat64(top) 88 | m.Params[2].SetValueFloat64(right) 89 | m.Params[3].SetValueFloat64(bottom) 90 | m.RequestPaint() 91 | 92 | } 93 | img, _, _, _, _ := RenderLSystem(left, top, right, bottom, bounds, angle, magnitude, result) 94 | return img 95 | } 96 | 97 | func RenderLSystem(left float64, top float64, right float64, bottom float64, bounds image.Rectangle, angle float64, magnitude float64, lsystem string) (*image.RGBA, float64, float64, float64, float64) { 98 | 99 | b := image.NewRGBA(bounds) 100 | 101 | gc := draw2dimg.NewGraphicContext(b) 102 | gc.SetFillColor(color.White) 103 | gc.Clear() 104 | gc.SetStrokeColor(color.Black) 105 | gc.SetLineWidth(1) 106 | 107 | dx := right - left 108 | //dy := bottom - top 109 | mx := float64(bounds.Dx()) / dx 110 | my := float64(bounds.Dx()) / dx 111 | 112 | location := FPoint{0, 0} 113 | nextLocation := FPoint{0, 0} 114 | direction := 90.0 115 | theta := direction * radcon 116 | 117 | maxX := 0.0 118 | minX := 0.0 119 | maxY := 0.0 120 | minY := 0.0 121 | 122 | stack := make([]State, 0, 100) 123 | 124 | for _, rn := range lsystem { 125 | theta = direction * radcon 126 | switch rn { 127 | case '0', '1', '2', '3', '4', '5', 'A', 'B', 'C', 'D', 'E', 'F', 'G': 128 | nextLocation = location.Add(FPoint{magnitude * math.Cos(theta), magnitude * math.Sin(theta)}) 129 | gc.MoveTo((location.X-left)*mx, (location.Y-top)*my) 130 | gc.LineTo((nextLocation.X-left)*mx, (nextLocation.Y-top)*my) 131 | //fmt.Printf("Drawing from %v to %v\n", location, nextLocation) 132 | location = nextLocation 133 | case 'a', 'b', 'c', 'd', 'e', 'f', 'g': 134 | nextLocation = location.Add(FPoint{magnitude * math.Cos(theta), magnitude * math.Sin(theta)}) 135 | location = nextLocation 136 | case '+': 137 | direction -= angle 138 | case '-': 139 | direction += angle 140 | case '[': 141 | stack = append(stack, State{location, direction}) 142 | case ']': 143 | if len(stack) > 0 { 144 | state := stack[len(stack)-1] 145 | location = state.Location 146 | direction = state.Direction 147 | stack = stack[:len(stack)-1] 148 | } 149 | } 150 | maxX = FMax(maxX, location.X) 151 | minX = FMin(minX, location.X) 152 | maxY = FMax(maxY, location.Y) 153 | minY = FMin(minY, location.Y) 154 | //fmt.Printf("(%v),", location) 155 | } 156 | gc.Stroke() 157 | //fmt.Printf("%v, %v, %v, %v, %v, %v\n", minX, minY, maxX, maxY, location.X, location.Y) 158 | 159 | return b, minX, minY, maxX - minX, maxY - minY 160 | 161 | } 162 | -------------------------------------------------------------------------------- /examples/mandelbrot/LICENSE_skeys: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012 Sonia Keys 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /examples/mandelbrot/README.md: -------------------------------------------------------------------------------- 1 | This code is used by the demo - build under renderview/cmd/demo. 2 | -------------------------------------------------------------------------------- /examples/mandelbrot/mandelbrot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Sonia Keys. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE_skeys file. 4 | 5 | package mandelbrot 6 | 7 | // https://soniacodes.wordpress.com/ 8 | // https://rosettacode.org/wiki/Mandelbrot_set#Go 9 | // minor changes to make it a callable function 10 | 11 | import ( 12 | "image" 13 | "image/color" 14 | "image/draw" 15 | "math/cmplx" 16 | ) 17 | 18 | func mandelbrot(a complex128, maxEsc float64) float64 { 19 | i := 0.0 20 | for z := a; cmplx.Abs(z) < 2 && i < maxEsc; i++ { 21 | z = z*z + a 22 | } 23 | return float64(maxEsc-i) / maxEsc 24 | } 25 | 26 | func generateMandelbrot(rMin, iMin, rMax, iMax float64, width, red, green, blue int, maxEsc int) image.Image { 27 | scale := float64(width) / (rMax - rMin) 28 | height := int(scale * (iMax - iMin)) 29 | bounds := image.Rect(0, 0, width, height) 30 | b := image.NewRGBA(bounds) 31 | draw.Draw(b, bounds, image.NewUniform(color.Black), image.ZP, draw.Src) 32 | for x := 0; x < width; x++ { 33 | for y := 0; y < height; y++ { 34 | fEsc := mandelbrot(complex( 35 | float64(x)/scale+rMin, 36 | float64(y)/scale+iMin), float64(maxEsc)) 37 | b.Set(x, y, color.RGBA{uint8(float64(red) * fEsc), 38 | uint8(float64(green) * fEsc), uint8(float64(blue) * fEsc), 255}) 39 | 40 | } 41 | } 42 | return b 43 | } 44 | -------------------------------------------------------------------------------- /examples/mandelbrot/mandelmodel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package mandelbrot 6 | 7 | import ( 8 | "math" 9 | 10 | rv "github.com/TheGrum/renderview" 11 | ) 12 | 13 | type MandelModel rv.BasicRenderModel 14 | 15 | func getInnerRenderFunc(m *MandelModel) func() { 16 | return func() { 17 | innerRender(m) 18 | } 19 | } 20 | 21 | func innerRender(m *MandelModel) { 22 | var rMin, iMin, rMax, iMax float64 23 | var width, red, green, blue int 24 | var maxEsc int 25 | 26 | m.Lock() 27 | maxEsc = m.Params[4].GetValueInt() 28 | rMin = m.Params[0].GetValueFloat64() 29 | iMin = m.Params[1].GetValueFloat64() 30 | rMax = m.Params[2].GetValueFloat64() 31 | iMax = m.Params[3].GetValueFloat64() 32 | width = m.Params[5].GetValueInt() 33 | red = m.Params[7].GetValueInt() 34 | green = m.Params[8].GetValueInt() 35 | blue = m.Params[9].GetValueInt() 36 | m.Rendering = true 37 | m.Unlock() 38 | 39 | i2 := generateMandelbrot(rMin, iMin, rMax, iMax, width, red, green, blue, maxEsc) 40 | 41 | m.Lock() 42 | m.Img = i2 43 | m.Rendering = false 44 | m.Unlock() 45 | if !(m.RequestPaint == nil) { 46 | m.RequestPaint() 47 | } 48 | } 49 | 50 | func NewMandelModel() *rv.BasicRenderModel { 51 | m := rv.NewBasicRenderModel() 52 | m.InnerRender = getInnerRenderFunc((*MandelModel)(m)) 53 | m.AddParameters( 54 | rv.NewFloat64RP("left", -2), 55 | rv.NewFloat64RP("top", -1), 56 | rv.NewFloat64RP("right", 0.5), 57 | rv.NewFloat64RP("bottom", 1), 58 | rv.NewIntRP("maxEsc", 100), 59 | rv.NewIntRP("width", 100), 60 | rv.NewIntRP("height", 100), 61 | rv.NewIntRP("red", 230), 62 | rv.NewIntRP("green", 235), 63 | rv.NewIntRP("blue", 255), 64 | rv.NewFloat64RP("mouseX", 0), 65 | rv.NewFloat64RP("mouseY", 0), 66 | NewZoomRP("zoom", 1, (*MandelModel)(m)), 67 | rv.NewIntRP("options", rv.OPT_NONE)) 68 | go m.GoRender() 69 | return m 70 | } 71 | 72 | // Many applications can simply use OPT_AUTO_ZOOM 73 | // but since the Mandelbrot algorithm we are using ignores the height 74 | // and produces a square image, we use a custom parameter 75 | // to calculate the zoom ourselves, also taking the opportunity to 76 | // dynamically adjust the Escape parameter. 77 | type ZoomRenderParameter struct { 78 | rv.EmptyParameter 79 | 80 | Value int 81 | Model *MandelModel 82 | } 83 | 84 | func (e *ZoomRenderParameter) GetValueInt() int { 85 | return e.Value 86 | } 87 | 88 | func (e *ZoomRenderParameter) SetValueInt(v int) int { 89 | dz := float64(v - e.Value) 90 | if dz < 0 { 91 | dz = 1.1 92 | } else if dz > 0 { 93 | dz = 0.9 94 | } else { 95 | return v 96 | } 97 | 98 | e.Value = v 99 | 100 | rMin := e.Model.Params[0].GetValueFloat64() 101 | iMin := e.Model.Params[1].GetValueFloat64() 102 | rMax := e.Model.Params[2].GetValueFloat64() 103 | //iMax := e.Model.Params[3].GetValueFloat64() 104 | width := e.Model.Params[5].GetValueInt() 105 | //height := e.Model.Params[6].GetValueInt() 106 | mouseX := e.Model.Params[10].GetValueFloat64() 107 | mouseY := e.Model.Params[11].GetValueFloat64() 108 | 109 | zwidth := rMax - rMin 110 | //zheight := iMax - iMin 111 | nzwidth := zwidth * dz 112 | //nzheight := zheight * dz 113 | 114 | cx := mouseX / float64(width) 115 | cy := mouseY / float64(width) 116 | 117 | nleft := rMin - ((nzwidth - zwidth) * cx) 118 | nright := nleft + nzwidth 119 | ntop := iMin - ((nzwidth - zwidth) * cy) 120 | nbottom := ntop + nzwidth 121 | e.Model.Params[0].SetValueFloat64(nleft) 122 | e.Model.Params[1].SetValueFloat64(ntop) 123 | e.Model.Params[2].SetValueFloat64(nright) 124 | e.Model.Params[3].SetValueFloat64(nbottom) 125 | e.Model.Params[4].SetValueInt(100 + int(math.Pow(1.1, float64(v)))) 126 | 127 | e.Model.RequestPaint() 128 | 129 | return e.Value 130 | } 131 | 132 | func NewZoomRP(name string, value int, m *MandelModel) *ZoomRenderParameter { 133 | return &ZoomRenderParameter{ 134 | EmptyParameter: rv.EmptyParameter{ 135 | Name: name, 136 | Type: "int", 137 | }, 138 | Value: value, 139 | Model: m, 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /examples/maze/maze.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package main 6 | 7 | import "math/rand" 8 | 9 | // Walls 10 | const ( 11 | W_NONE = iota 12 | W_NORTH = 1 << iota 13 | W_EAST = 1 << iota 14 | W_SOUTH = 1 << iota 15 | W_WEST = 1 << iota 16 | W_VISITED = 1 << iota 17 | W_START = 1 << iota 18 | W_END = 1 << iota 19 | W_ALL = W_NORTH | W_EAST | W_SOUTH | W_WEST 20 | ) 21 | 22 | // Directions 23 | const ( 24 | D_NORTH = iota 25 | D_EAST = iota 26 | D_SOUTH = iota 27 | D_WEST = iota 28 | ) 29 | 30 | type Maze struct { 31 | width int 32 | height int 33 | 34 | cells []byte 35 | } 36 | 37 | // C returns the current value of cell x,y 38 | func (m *Maze) C(x int, y int) byte { 39 | return m.cells[y*m.width+x] 40 | } 41 | 42 | // A adds flag v to the cell x,y 43 | func (m *Maze) A(x int, y int, v byte) { 44 | m.cells[y*m.width+x] = m.cells[y*m.width+x] | v 45 | } 46 | 47 | // U removes flag v from the cell x,y 48 | func (m *Maze) U(x int, y int, v byte) { 49 | m.cells[y*m.width+x] = m.cells[y*m.width+x] & (^v) 50 | } 51 | 52 | // S returns whether the cell x,y has flag v set 53 | func (m *Maze) S(x int, y int, v byte) bool { 54 | return ((m.cells[y*m.width+x] & v) == v) 55 | } 56 | 57 | func NewDepthFirstMaze(width int, height int) *Maze { 58 | m := Maze{ 59 | width: width, 60 | height: height, 61 | 62 | cells: make([]byte, width*height), 63 | } 64 | 65 | for i := 0; i < width*height; i++ { 66 | m.cells[i] = W_EAST | W_SOUTH 67 | } 68 | 69 | x, y := 0, 0 70 | x = rand.Intn(width) 71 | y = rand.Intn(height) 72 | m.cells[y*width+x] = m.cells[y*width+x] | W_START 73 | DepthFirstStep(&m, x, y) 74 | for i := 0; i < width; i++ { 75 | m.A(i, 0, W_NORTH) 76 | } 77 | for i := 0; i < height; i++ { 78 | m.A(0, i, W_WEST) 79 | } 80 | return &m 81 | } 82 | 83 | func DepthFirstStep(m *Maze, x int, y int) { 84 | // fmt.Printf("x: %v, y: %v, s: %v %v %v\n", x, y, m.S(x, y, W_VISITED), W_VISITED, m.C(x, y)&W_VISITED) 85 | m.A(x, y, W_VISITED) 86 | // fmt.Printf("v: %v, s: %v\n", m.C(x, y), m.S(x, y, W_VISITED)) 87 | // Loop over each direction, recursing into each 88 | // cell 89 | for _, d := range rand.Perm(4) { 90 | switch d { 91 | case D_NORTH: 92 | // if we aren't at the top already 93 | if y > 0 { 94 | if !m.S(x, y-1, W_VISITED) { 95 | m.U(x, y-1, W_SOUTH) 96 | DepthFirstStep(m, x, y-1) 97 | } 98 | 99 | } 100 | case D_EAST: 101 | if x < m.width-1 { 102 | if !m.S(x+1, y, W_VISITED) { 103 | m.U(x, y, W_EAST) 104 | DepthFirstStep(m, x+1, y) 105 | } 106 | } 107 | case D_SOUTH: 108 | if y < m.height-1 { 109 | if !m.S(x, y+1, W_VISITED) { 110 | m.U(x, y, W_SOUTH) 111 | DepthFirstStep(m, x, y+1) 112 | } 113 | } 114 | case D_WEST: 115 | if x > 0 { 116 | if !m.S(x-1, y, W_VISITED) { 117 | m.U(x-1, y, W_EAST) 118 | DepthFirstStep(m, x-1, y) 119 | } 120 | } 121 | } 122 | 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /examples/maze/mazegui.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "image" 10 | "image/color" 11 | "math/rand" 12 | "time" 13 | 14 | rv "github.com/TheGrum/renderview" 15 | "github.com/TheGrum/renderview/driver" 16 | 17 | "github.com/llgcode/draw2d/draw2dimg" 18 | ) 19 | 20 | type ff float64 21 | 22 | func main() { 23 | sig := "" 24 | rand.Seed(time.Now().UnixNano()) 25 | m := rv.NewBasicRenderModel() 26 | m.AddParameters( 27 | rv.SetHints(rv.HINT_HIDE, 28 | rv.NewIntRP("width", 0), 29 | rv.NewIntRP("height", 0), 30 | )...) 31 | m.AddParameters( 32 | rv.NewIntRP("page", 0), 33 | rv.NewIntRP("linewidth", 1), 34 | rv.NewIntRP("cellwidth", 5), 35 | rv.NewIntRP("mazewidth", 100), 36 | rv.NewIntRP("mazeheight", 100)) 37 | m.InnerRender = func() { 38 | s := GetSignature(m) 39 | if sig != s { 40 | z := NewDepthFirstMaze(m.Params[5].GetValueInt(), m.Params[6].GetValueInt()) 41 | m.Img = RenderMaze(m, z) 42 | sig = s 43 | m.RequestPaint() 44 | } 45 | } 46 | //driver.Main(rv.GetWidgetMainLoop(m)) 47 | //rv.GtkWindowWithWidgetsInit(m) 48 | driver.Main(m) 49 | } 50 | 51 | func GetSignature(r *rv.BasicRenderModel) string { 52 | s := "" 53 | for i := 0; i < len(r.Params); i++ { 54 | p := r.Params[i] 55 | switch p.GetType() { 56 | case "int": 57 | s = fmt.Sprintf("%s%v", s, p.GetValueInt()) 58 | case "uint32": 59 | s = fmt.Sprintf("%s%v", s, p.GetValueUInt32()) 60 | case "float64": 61 | s = fmt.Sprintf("%s%v", s, p.GetValueFloat64()) 62 | case "complex128": 63 | s = fmt.Sprintf("%s%v", s, p.GetValueComplex128()) 64 | case "string": 65 | s = fmt.Sprintf("%s%v", s, p.GetValueString()) 66 | } 67 | } 68 | return s 69 | } 70 | 71 | func RenderMaze(r rv.RenderModel, m *Maze) image.Image { 72 | // w := m.width 73 | // h := m.height 74 | mw := r.GetParameter("mazewidth").GetValueInt() 75 | mh := r.GetParameter("mazeheight").GetValueInt() 76 | lw := r.GetParameter("linewidth").GetValueInt() 77 | cw := r.GetParameter("cellwidth").GetValueInt() 78 | 79 | iw := (mw * cw) + ((mw + 2) * lw) 80 | ih := (mh * cw) + ((mh + 2) * lw) 81 | 82 | bounds := image.Rect(0, 0, iw, ih) 83 | b := image.NewRGBA(bounds) 84 | 85 | gc := draw2dimg.NewGraphicContext(b) 86 | gc.SetFillColor(color.White) 87 | gc.Clear() 88 | gc.SetStrokeColor(color.Black) 89 | gc.SetLineWidth(float64(lw)) 90 | 91 | for x := 0; x < mw; x++ { 92 | for y := 0; y < mh; y++ { 93 | v := m.C(x, y) 94 | if v&W_NORTH == W_NORTH { 95 | gc.MoveTo(float64(x*(cw+(lw))), float64(y*(cw+(lw)))) 96 | gc.LineTo(float64((x+1)*(cw+(lw))), float64(y*(cw+(lw)))) 97 | } 98 | if v&W_EAST == W_EAST { 99 | gc.MoveTo(float64((x+1)*(cw+(lw))), float64(y*(cw+(lw)))) 100 | gc.LineTo(float64((x+1)*(cw+(lw))), float64((y+1)*(cw+(lw)))) 101 | } 102 | 103 | if v&W_SOUTH == W_SOUTH { 104 | gc.MoveTo(float64(x*(cw+(lw))), float64((y+1)*(cw+(lw)))) 105 | gc.LineTo(float64((x+1)*(cw+(lw))), float64((y+1)*(cw+(lw)))) 106 | } 107 | 108 | if v&W_WEST == W_WEST { 109 | gc.MoveTo(float64(x*(cw+(lw))), float64(y*(cw+(lw)))) 110 | gc.LineTo(float64(x*(cw+(lw))), float64((y+1)*(cw+(lw)))) 111 | } 112 | } 113 | } 114 | 115 | gc.Stroke() 116 | 117 | return b 118 | } 119 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/TheGrum/renderview 2 | 3 | go 1.13 4 | 5 | require ( 6 | gioui.org v0.0.0-20200210173153-f38dbfca544c 7 | github.com/gotk3/gotk3 v0.0.0-20200210190119-513f671252e2 8 | github.com/llgcode/draw2d v0.0.0-20200110163050-b96d8208fcfc 9 | github.com/mattn/go-gtk v0.0.0-20191030024613-af2e013261f5 10 | github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f // indirect 11 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd 12 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b 13 | golang.org/x/mobile v0.0.0-20200205170228-0df4eb238546 14 | ) 15 | -------------------------------------------------------------------------------- /model/tile/README.md: -------------------------------------------------------------------------------- 1 | # TileRenderModel # 2 | ===================== 3 | 4 | TileRenderModel provides a model that consumes a set of interfaces to make displaying map tiles easy. 5 | 6 | ![map](http://i.imgur.com/MIwJRa5.png) 7 | 8 | ## LatLon 9 | 10 | A simple wrapper on a float64 latitude and longitude. 11 | 12 | ## Tile 13 | 14 | A Tile is a trio of integers defining an X, Y, and Z coordinate, where Z is a zoom, and X and Y vary between 0 and 2^Z-1. It provides helper functions for finding the tile at a lower zoom level that contains this tile, or the 4 child tiles contained within this tile at the next higher zoom level. 15 | 16 | ## TileMapper 17 | 18 | A TileMapper provides the functions necessary for converting between LatLons and Tiles. 19 | 20 | Most usecases can just use tile.OSM for this. 21 | 22 | ## TileProvider 23 | 24 | A TileProvider renders a given Tile and returns an Image. 25 | 26 | ### AdvancedTileProvider 27 | 28 | A TileProvider that can also render a range of tiles. 29 | 30 | ### StreamingTileProvider 31 | 32 | A TileProvider that can render a range of tiles and return them down a channel. 33 | 34 | ### FallbackTileProvider 35 | 36 | An AdvancedTileProvider that can fall back to an alternate provider when a rendering fails. 37 | 38 | ### TestTileProvider 39 | 40 | Renders an image of a specified size with the tile coordinates drawn on it, for testing. 41 | 42 | ### OSMTileProvider 43 | 44 | The only concrete non-test provider currently. Accepts a Server URL for use to obtain tile images, replacing $X, $Y, and $Z in the URL with the tile coordinates. 45 | 46 | ### CompositingTileProvider 47 | 48 | Takes a list of TileProviders and renders them in order, compositing later images onto earlier ones in order. 49 | 50 | ## TileCache 51 | 52 | A LeastRecentlyUsed in-memory tile cache that fulfills the AdvancedTileProvider interface, takes a TileProvider to perform the actual rendering. 53 | 54 | ### StreamingTileCache 55 | 56 | Implements the StreamingTileProvider over a TileCache. 57 | 58 | ### FallbackTileCache 59 | 60 | Implements the FallbackTileProvider over a primary TileProvider and a fallback TileProvider, responding with fallback tiles and putting rendering requests in a queue to render in the background. 61 | 62 | ## TileRenderModel 63 | 64 | Takes a TileMapper and TileProvider and handles requesting and merging tiles into a single image to return to the RenderView. 65 | -------------------------------------------------------------------------------- /model/tile/tile.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "math" 4 | 5 | type Tile struct { 6 | X uint 7 | Y uint 8 | Z uint 9 | } 10 | 11 | func (t Tile) CompareTo(a Tile) int { 12 | if t.Z < a.Z { 13 | return -1 14 | } 15 | if t.Z > a.Z { 16 | return 1 17 | } 18 | // zoomLevels are equal 19 | if t.Y < a.Y { 20 | return -1 21 | } 22 | if t.Y > a.Y { 23 | return 1 24 | } 25 | // latitudes are equal 26 | if t.X < a.X { 27 | return -1 28 | } 29 | if t.X > a.X { 30 | return 1 31 | } 32 | // all are equal 33 | return 0 34 | } 35 | 36 | func (t Tile) IsInside(a Tile, b Tile) bool { 37 | if t.Z != a.Z || t.Z != b.Z { 38 | return false 39 | } 40 | if (t.X >= a.X) && (t.X <= b.X) && (t.Y >= a.Y) && (t.Y <= b.Y) { 41 | return true 42 | } 43 | return false 44 | } 45 | 46 | func (t Tile) GetParentTile() Tile { 47 | if t.Z == 0 { 48 | return t 49 | } else { 50 | return Tile{ 51 | Z: t.Z - 1, 52 | Y: t.Y / 2, 53 | X: t.X / 2, 54 | } 55 | } 56 | } 57 | 58 | func (t Tile) GetChildTile(left bool, top bool) Tile { 59 | switch true { 60 | case left && top: 61 | return Tile{ 62 | Z: t.Z + 1, 63 | Y: t.Y * 2, 64 | X: t.X * 2, 65 | } 66 | case !left && top: 67 | return Tile{ 68 | Z: t.Z + 1, 69 | Y: t.Y * 2, 70 | X: t.X*2 + 1, 71 | } 72 | case left && !top: 73 | return Tile{ 74 | Z: t.Z + 1, 75 | Y: t.Y*2 + 1, 76 | X: t.X * 2, 77 | } 78 | default: 79 | return Tile{ 80 | Z: t.Z + 1, 81 | Y: t.Y*2 + 1, 82 | X: t.X*2 + 1, 83 | } 84 | } 85 | } 86 | 87 | type LatLon struct { 88 | Lat float64 89 | Lon float64 90 | } 91 | 92 | type TileMapper interface { 93 | LatLon(t Tile) LatLon 94 | Tile(l LatLon, Zoom uint) Tile 95 | TilesFromBounds(a LatLon, b LatLon, maxWidth uint, maxHeight uint) (Tile, Tile) 96 | BoundsFromTiles(a Tile, b Tile) (LatLon, LatLon) 97 | } 98 | 99 | func ShiftToBottomRight(t Tile) Tile { 100 | max := uint(2< 0) && ((l > maxWidth) || (m > maxHeight)) { 143 | zoomLevel -= 1 144 | } else { 145 | break 146 | } 147 | } 148 | // push d to bottom right since left/top can actually be 149 | // within tile c, shifting image up and to the left 150 | return c, ShiftToBottomRight(d) 151 | } 152 | 153 | // GenericBoundsFromTiles returns the lat/lon pairs that encompass 154 | // a set of tiles 155 | func GenericBoundsFromTiles(t TileMapper, a Tile, b Tile, rightbottom LatLon) (LatLon, LatLon) { 156 | // left is easy 157 | var c, d LatLon 158 | c = t.LatLon(a) 159 | max := uint(2< (2< (2<= c.maxItems { 141 | // drop oldest items 142 | c.cache = c.cache[(len(c.cache)-int(c.maxItems))+1:] 143 | } 144 | i := TileImage{true, t, img} 145 | c.cache = append(c.cache, i) 146 | return i 147 | } 148 | 149 | /* 150 | func ZoomFilter(c []TileImage, zoomLevel uint) []TileImage { 151 | r := make([]TileImage, 0, len(c)) 152 | for _, k := range c { 153 | if k.Tile.Z == zoomLevel { 154 | r = append(r, k) 155 | } 156 | } 157 | return r 158 | } 159 | */ 160 | 161 | // SwapIfNeeded is a utility function that ensures we have the upper left 162 | // and bottom right tiles 163 | func SwapIfNeeded(a Tile, b Tile) (Tile, Tile) { 164 | if b.Y < a.Y { 165 | a, b = b, a 166 | } 167 | // Now a.Y must be less then b.Y, 168 | // we want a.X to be less then b.X as well 169 | if b.X < a.X { 170 | a.X, b.X = b.X, a.X 171 | } 172 | return a, b 173 | } 174 | 175 | // Renders all tiles in the requested range and returns them 176 | // This returns TileImages instead of merely images 177 | // because the items are returned in an indeterminate order 178 | // use the Tile to determine their position. a and b should 179 | // be on the same zoom level 180 | func (c *TileCache) RenderTileRange(a Tile, b Tile) []TileImage { 181 | if a.Z != b.Z { 182 | return nil 183 | } 184 | 185 | // make sure that we can compare to a and b to determine if tile t is contained within 186 | a, b = SwapIfNeeded(a, b) 187 | w := b.X - a.X + 1 188 | h := b.Y - a.Y + 1 189 | 190 | // We don't use ZoomFilter(c.cache, a.Z) here 191 | // because we want to efficiently move the found 192 | // items forward in the cache 193 | nfcache := make([]TileImage, 0, len(c.cache)) 194 | fcache := make([]TileImage, 0, len(c.cache)) 195 | 196 | // remember what we've found so far in here so 197 | // we know what to go and render at the end 198 | found := make([]TileImage, w*h, w*h) 199 | 200 | for _, k := range c.cache { 201 | if k.Tile.IsInside(a, b) { 202 | // this is one of the requested images 203 | found[(k.Tile.Y-a.Y)*w+(k.Tile.X-a.X)] = k 204 | fcache = append(fcache, k) 205 | } else { 206 | nfcache = append(nfcache, k) 207 | } 208 | } 209 | 210 | // Render all missing tiles and add them to the fcache 211 | for i := 0; i < int(h); i++ { 212 | for j := 0; j < int(w); j++ { 213 | //fmt.Printf("Checking i %d, j %d, kpos %d\n", i, j, i*int(w)+j) 214 | k := found[i*int(w)+j] 215 | //fmt.Printf("Checking k(%v)\n", k) 216 | if k.populated == false { 217 | tile := Tile{a.X + uint(j), a.Y + uint(i), a.Z} 218 | k = TileImage{true, tile, c.provider.RenderTile(tile)} 219 | found[i*int(w)+j] = k 220 | fcache = append(fcache, k) 221 | } 222 | } 223 | } 224 | 225 | // At the end of this process, fcache holds all the returned tiles 226 | // and nfcache holds what was not returned, so for LRU, 227 | // we want the last maxItems from nfcache+fcache 228 | fnum := len(fcache) 229 | nnum := len(nfcache) 230 | 231 | if fnum > int(c.maxItems) { 232 | fnum = int(c.maxItems) 233 | nnum = 0 234 | } else if nnum+fnum > int(c.maxItems) { 235 | nnum = int(c.maxItems) - fnum 236 | } 237 | 238 | if nnum > 0 { 239 | c.cache = append(nfcache[len(nfcache)-nnum:], fcache...) 240 | } else { 241 | c.cache = fcache[len(fcache)-fnum:] 242 | } 243 | 244 | return found 245 | } 246 | 247 | // Renders all tiles in the requested range and returns them 248 | // This returns TileImages instead of merely images 249 | // because the items are returned in an indeterminate order 250 | // use the Tile to determine their position. a and b should 251 | // be on the same zoom level 252 | func (c *StreamingTileCache) StreamTileRange(a Tile, b Tile) chan TileImage { 253 | ch := make(chan TileImage, 20) 254 | go func() { 255 | if a.Z != b.Z { 256 | close(ch) 257 | return 258 | } 259 | 260 | // make sure that we can compare to a and b to determine if tile t is contained within 261 | a, b = SwapIfNeeded(a, b) 262 | w := b.X - a.X + 1 263 | h := b.Y - a.Y + 1 264 | 265 | // We don't use ZoomFilter(c.cache, a.Z) here 266 | // because we want to efficiently move the found 267 | // items forward in the cache 268 | nfcache := make([]TileImage, 0, len(c.cache)) 269 | fcache := make([]TileImage, 0, len(c.cache)) 270 | 271 | // remember what we've found so far in here so 272 | // we know what to go and render at the end 273 | found := make([]TileImage, w*h, w*h) 274 | 275 | for _, k := range c.cache { 276 | if k.Tile.IsInside(a, b) { 277 | // this is one of the requested images 278 | found[(k.Tile.Y-a.Y)*w+(k.Tile.X-a.X)] = k 279 | ch <- k 280 | fcache = append(fcache, k) 281 | } else { 282 | nfcache = append(nfcache, k) 283 | } 284 | } 285 | 286 | // Render all missing tiles and add them to the fcache 287 | for i := 0; i < int(h); i++ { 288 | for j := 0; j < int(w); j++ { 289 | //fmt.Printf("Checking i %d, j %d, kpos %d\n", i, j, i*int(w)+j) 290 | k := found[i*int(w)+j] 291 | //fmt.Printf("Checking k(%v)\n", k) 292 | if k.populated == false { 293 | tile := Tile{a.X + uint(j), a.Y + uint(i), a.Z} 294 | k = TileImage{true, tile, c.provider.RenderTile(tile)} 295 | ch <- k 296 | found[i*int(w)+j] = k 297 | fcache = append(fcache, k) 298 | } 299 | } 300 | } 301 | 302 | // At the end of this process, fcache holds all the returned tiles 303 | // and nfcache holds what was not returned, so for LRU, 304 | // we want the last maxItems from nfcache+fcache 305 | fnum := len(fcache) 306 | nnum := len(nfcache) 307 | 308 | if fnum > int(c.maxItems) { 309 | fnum = int(c.maxItems) 310 | nnum = 0 311 | } else if nnum+fnum > int(c.maxItems) { 312 | nnum = int(c.maxItems) - fnum 313 | } 314 | 315 | if nnum > 0 { 316 | c.cache = append(nfcache[len(nfcache)-nnum:], fcache...) 317 | } else { 318 | c.cache = fcache[len(fcache)-fnum:] 319 | } 320 | close(ch) 321 | 322 | }() 323 | 324 | return ch 325 | } 326 | 327 | // Renders all tiles in the requested range and returns them 328 | // This returns TileImages instead of merely images 329 | // because the items are returned in an indeterminate order 330 | // use the Tile to determine their position. a and b should 331 | // be on the same zoom level 332 | func (c *FallbackTileCache) RenderTileRange(a Tile, b Tile) []TileImage { 333 | if a.Z != b.Z { 334 | return nil 335 | } 336 | 337 | // make sure that we can compare to a and b to determine if tile t is contained within 338 | a, b = SwapIfNeeded(a, b) 339 | w := b.X - a.X + 1 340 | h := b.Y - a.Y + 1 341 | 342 | // We don't use ZoomFilter(c.cache, a.Z) here 343 | // because we want to efficiently move the found 344 | // items forward in the cache 345 | nfcache := make([]TileImage, 0, len(c.cache)) 346 | fcache := make([]TileImage, 0, len(c.cache)) 347 | 348 | // remember what we've found so far in here so 349 | // we know what to go and render at the end 350 | found := make([]TileImage, w*h, w*h) 351 | 352 | c.l.Lock() 353 | for _, k := range c.cache { 354 | if k.Tile.IsInside(a, b) { 355 | // this is one of the requested images 356 | found[(k.Tile.Y-a.Y)*w+(k.Tile.X-a.X)] = k 357 | fcache = append(fcache, k) 358 | } else { 359 | nfcache = append(nfcache, k) 360 | } 361 | } 362 | // At the end of this process, fcache holds all the returned tiles 363 | // and nfcache holds what was not returned, so for LRU, 364 | // we want the last maxItems from nfcache+fcache 365 | fnum := len(fcache) 366 | nnum := len(nfcache) 367 | 368 | if fnum > int(c.maxItems) { 369 | fnum = int(c.maxItems) 370 | nnum = 0 371 | } else if nnum+fnum > int(c.maxItems) { 372 | nnum = int(c.maxItems) - fnum 373 | } 374 | 375 | if nnum > 0 { 376 | c.cache = append(nfcache[len(nfcache)-nnum:], fcache...) 377 | } else { 378 | c.cache = fcache[len(fcache)-fnum:] 379 | } 380 | c.l.Unlock() 381 | 382 | c.usedFallback = false 383 | 384 | // Render all missing tiles using the fallback provider 385 | // Also add them to the request queue 386 | // Note that this results in items being added to the queue 387 | // more often than needed, so the queue servicing should 388 | // avoid rendering items that are still in the cache 389 | // don't add them to the fcache 390 | for i := 0; i < int(h); i++ { 391 | for j := 0; j < int(w); j++ { 392 | k := found[i*int(w)+j] 393 | if k.populated == false { 394 | tile := Tile{a.X + uint(j), a.Y + uint(i), a.Z} 395 | k = TileImage{true, tile, c.fallback.RenderTile(tile)} 396 | c.requests <- tile 397 | found[i*int(w)+j] = k 398 | c.usedFallback = true 399 | // Don't add to cache, because these are fallback images 400 | //fcache = append(fcache, k) 401 | } 402 | } 403 | } 404 | 405 | return found 406 | } 407 | 408 | func (c *FallbackTileCache) UsedFallback() bool { 409 | return c.usedFallback 410 | } 411 | 412 | // RenderTile returns an image from Cache, or if not found 413 | // requests it from the TileProvider and returns it 414 | func (c *FallbackTileCache) RenderTile(t Tile) image.Image { 415 | for i, k := range c.cache { 416 | if k.CompareTo(t) == 0 { 417 | // found the item, grab it 418 | r := k 419 | c.l.Lock() 420 | // pull it out of the list 421 | copy(c.cache[i:], c.cache[i+1:]) 422 | // and put it back at the end 423 | c.cache = append(c.cache[:len(c.cache)-1], r) 424 | c.l.Unlock() 425 | return r.Img 426 | } 427 | } 428 | k := c.render(t) 429 | return k.Img 430 | } 431 | 432 | // Private function assumes that cache does NOT contain 433 | // t; requests t from Provider and adds to cache, 434 | // removing oldest item if necessary 435 | func (c *FallbackTileCache) render(t Tile) TileImage { 436 | img := c.provider.RenderTile(t) 437 | c.l.Lock() 438 | if uint(len(c.cache)) >= c.maxItems { 439 | // drop oldest items 440 | c.cache = c.cache[(len(c.cache)-int(c.maxItems))+1:] 441 | } 442 | i := TileImage{true, t, img} 443 | c.cache = append(c.cache, i) 444 | c.l.Unlock() 445 | return i 446 | } 447 | 448 | // GoRender is called by NewFallbackTileCache and renders tiles 449 | // from the request queue and stores them in the cache 450 | func (c *FallbackTileCache) GoRender() { 451 | for k := range c.requests { 452 | // RenderTile has the behavior we want 453 | // of checking the cache first, and adding the image 454 | // from the primary TileProvider only 455 | // if it is not found in the cache 456 | _ = c.RenderTile(k) 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /model/tile/tileprovider.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package model 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "image" 11 | "image/color" 12 | "image/draw" 13 | "image/png" 14 | "io/ioutil" 15 | "log" 16 | "net" 17 | "net/http" 18 | "strconv" 19 | "strings" 20 | 21 | "github.com/llgcode/draw2d/draw2dimg" 22 | ) 23 | 24 | type TileProvider interface { 25 | RenderTile(t Tile) image.Image 26 | } 27 | 28 | type TestTileProvider struct { 29 | Width int 30 | Height int 31 | } 32 | 33 | func NewTestTileProvider(width, height int) *TestTileProvider { 34 | return &TestTileProvider{ 35 | Width: width, 36 | Height: height, 37 | } 38 | } 39 | 40 | func (p *TestTileProvider) RenderTile(t Tile) image.Image { 41 | img := image.NewRGBA(image.Rect(0, 0, p.Width, p.Height)) 42 | w, h := float64(p.Width), float64(p.Height) 43 | gc := draw2dimg.NewGraphicContext(img) 44 | gc.SetFillColor(color.White) 45 | gc.Clear() 46 | gc.SetStrokeColor(color.Black) 47 | gc.SetLineWidth(1) 48 | 49 | gc.MoveTo(0, 0) 50 | gc.LineTo(w, 0) 51 | gc.LineTo(w, h) 52 | gc.LineTo(0, h) 53 | gc.LineTo(0, 0) 54 | gc.Stroke() 55 | gc.SetFillColor(color.Black) 56 | gc.SetFontSize(12) 57 | gc.MoveTo(0, 0) 58 | gc.FillStringAt(fmt.Sprintf("Tile %dZ, %dY, %dX", t.Z, t.Y, t.X), 10, 70) 59 | //fmt.Printf("Tile %dZ, %dY, %dX", t.Z, t.Y, t.X) 60 | return img 61 | } 62 | 63 | type OSMTileProvider struct { 64 | ServerURL string 65 | client *http.Client 66 | } 67 | 68 | func NewOSMTileProvider(ServerURL string) *OSMTileProvider { 69 | o := OSMTileProvider{ 70 | ServerURL: ServerURL, 71 | } 72 | o.client = &http.Client{ 73 | Transport: &http.Transport{ 74 | Dial: func(network, addr string) (net.Conn, error) { 75 | //log.Println("Dial!") 76 | return net.Dial(network, addr) 77 | }, 78 | MaxIdleConnsPerHost: 50, 79 | }, 80 | } 81 | return &o 82 | } 83 | 84 | func (o *OSMTileProvider) RenderTile(t Tile) image.Image { 85 | request := o.ServerURL 86 | request = strings.Replace(request, "$Z", strconv.Itoa(int(t.Z)), -1) 87 | request = strings.Replace(request, "$Y", strconv.Itoa(int(t.Y)), -1) 88 | request = strings.Replace(request, "$X", strconv.Itoa(int(t.X)), -1) 89 | resp, err := o.client.Get(request) 90 | if err != nil { 91 | log.Println(err) 92 | return image.NewRGBA(image.Rect(0, 0, 1, 1)) 93 | } 94 | defer resp.Body.Close() 95 | body, err := ioutil.ReadAll(resp.Body) 96 | if err != nil { 97 | log.Println(err) 98 | return image.NewRGBA(image.Rect(0, 0, 1, 1)) 99 | } 100 | tile, err := png.Decode(bytes.NewReader(body)) 101 | if err != nil { 102 | log.Println(err) 103 | return image.NewRGBA(image.Rect(0, 0, 1, 1)) 104 | } 105 | return tile 106 | } 107 | 108 | type CompositingTileProvider struct { 109 | providers []TileProvider 110 | } 111 | 112 | func NewCompositingTileProvider(providers ...TileProvider) *CompositingTileProvider { 113 | c := CompositingTileProvider{ 114 | providers: make([]TileProvider, len(providers), len(providers)), 115 | } 116 | copy(c.providers, providers) 117 | return &c 118 | } 119 | 120 | func (c *CompositingTileProvider) RenderTile(t Tile) image.Image { 121 | var img draw.Image 122 | for i, p := range c.providers { 123 | piece := p.RenderTile(t) 124 | if piece != nil { 125 | switch drawpiece := piece.(type) { 126 | case *image.RGBA: 127 | if i == 0 || img == nil { 128 | img = drawpiece 129 | } else { 130 | draw.Draw(img, img.Bounds(), drawpiece, image.ZP, draw.Over) 131 | } 132 | case *image.NRGBA: 133 | if i == 0 || img == nil { 134 | img = drawpiece 135 | } else { 136 | draw.Draw(img, img.Bounds(), drawpiece, image.ZP, draw.Over) 137 | } 138 | 139 | default: 140 | // not an RGBA or NRGBA 141 | if i == 0 || img == nil { 142 | // make one and copy to it 143 | img = image.NewRGBA(drawpiece.Bounds()) 144 | draw.Draw(img, img.Bounds(), drawpiece, image.ZP, draw.Src) 145 | } else { 146 | draw.Draw(img, img.Bounds(), drawpiece, image.ZP, draw.Over) 147 | } 148 | } 149 | } 150 | } 151 | return img 152 | } 153 | -------------------------------------------------------------------------------- /model/tile/tilerendermodel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package model 6 | 7 | import ( 8 | "image" 9 | "image/draw" 10 | "math" 11 | 12 | rv "github.com/TheGrum/renderview" 13 | ) 14 | 15 | type TileRenderModel struct { 16 | rv.EmptyRenderModel 17 | 18 | RequestRender chan interface{} 19 | Rendering bool 20 | NeedsRender bool 21 | Img image.Image 22 | mapper TileMapper 23 | provider TileProvider 24 | 25 | lastTopLeft Tile 26 | lastBottomRight Tile 27 | // lastWidth int 28 | // lastHeight int 29 | 30 | left rv.RenderParameter 31 | right rv.RenderParameter 32 | top rv.RenderParameter 33 | bottom rv.RenderParameter 34 | width rv.RenderParameter 35 | height rv.RenderParameter 36 | started bool 37 | } 38 | 39 | func NewTileRenderModel(mapper TileMapper, provider TileProvider, leftTop LatLon, bottomRight LatLon) *TileRenderModel { 40 | m := TileRenderModel{ 41 | EmptyRenderModel: rv.EmptyRenderModel{ 42 | Params: make([]rv.RenderParameter, 0, 10), 43 | }, 44 | RequestRender: make(chan interface{}, 10), 45 | mapper: mapper, 46 | provider: provider, 47 | started: false, 48 | } 49 | m.AddParameters(rv.DefaultParameters(false, rv.HINT_HIDE, rv.OPT_AUTO_ZOOM, leftTop.Lon, leftTop.Lat, bottomRight.Lon, bottomRight.Lat)...) 50 | m.AddParameters(rv.SetHints(rv.HINT_HIDE, rv.NewFloat64RP("zoomRate", 0.50))...) 51 | m.left = m.GetParameter("left") 52 | m.top = m.GetParameter("top") 53 | m.right = m.GetParameter("right") 54 | m.bottom = m.GetParameter("bottom") 55 | m.width = m.GetParameter("width") 56 | m.height = m.GetParameter("height") 57 | return &m 58 | } 59 | 60 | func (m *TileRenderModel) Render() image.Image { 61 | if !m.Rendering { 62 | m.RequestRender <- true 63 | m.NeedsRender = false 64 | } else { 65 | m.NeedsRender = true 66 | } 67 | return m.Img 68 | } 69 | 70 | func (m *TileRenderModel) Start() { 71 | if !m.started { 72 | m.started = true 73 | go m.GoRender() 74 | } 75 | } 76 | 77 | func (m *TileRenderModel) GoRender() { 78 | for { 79 | select { 80 | case <-m.RequestRender: 81 | m.InnerRender() 82 | } 83 | } 84 | } 85 | 86 | func (m *TileRenderModel) InnerRender() { 87 | 88 | img, ok := m.Img.(*image.RGBA) 89 | if img == nil || !ok { 90 | i2 := image.NewRGBA(image.Rect(0, 0, m.width.GetValueInt(), m.height.GetValueInt())) 91 | img = i2 92 | m.Img = img 93 | } else { 94 | b := img.Bounds() 95 | if b.Dx() != m.width.GetValueInt() || b.Dy() != m.height.GetValueInt() { 96 | i2 := image.NewRGBA(image.Rect(0, 0, m.width.GetValueInt(), m.height.GetValueInt())) 97 | // maybe do something here to copy/move the previous image? 98 | img = i2 99 | m.Img = img 100 | } 101 | } 102 | a := LatLon{m.top.GetValueFloat64(), m.left.GetValueFloat64()} 103 | b := LatLon{m.bottom.GetValueFloat64(), m.right.GetValueFloat64()} 104 | c, d := m.mapper.TilesFromBounds(a, b, uint(img.Bounds().Dx()), uint(img.Bounds().Dy())) 105 | modA, _ := m.mapper.BoundsFromTiles(c, d) 106 | ca, cb := m.mapper.BoundsFromTiles(c, c) 107 | 108 | var tileSizeX, tileSizeY float64 109 | var i, j int 110 | var offsetP image.Point 111 | m.Rendering = true 112 | w, h := d.X-c.X, d.Y-c.Y 113 | switch t := m.provider.(type) { 114 | case StreamingTileProvider: 115 | tilech := t.StreamTileRange(c, d) 116 | i = 0 117 | for k := range tilech { 118 | i2 := k.Img 119 | if (i == 0) && i2 != nil { 120 | tileSizeX = float64(i2.Bounds().Dx()) 121 | tileLonWidth := tileSizeX / (cb.Lon - ca.Lon) 122 | offsetP.X = int(math.Floor((modA.Lon - a.Lon) * tileLonWidth)) 123 | tileSizeY = float64(i2.Bounds().Dy()) 124 | tileLatHeight := tileSizeY / (cb.Lat - ca.Lat) 125 | offsetP.Y = int(math.Floor((modA.Lat - a.Lat) * tileLatHeight)) 126 | // Now correct right/bottom to actual screen range 127 | // m.left.SetValueFloat64(a.Lon + tileLonWidth*tileSizeX*float64(w)) 128 | // m.bottom.SetValueFloat64(a.Lat + tileLatHeight*tileSizeX*float64(h)) 129 | i = 1 130 | } 131 | i = int(k.Tile.Y - c.Y) 132 | j = int(k.Tile.X - c.X) 133 | i2 = k.Img 134 | draw.Draw(img, image.Rect(offsetP.X+j*int(tileSizeX), offsetP.Y+i*int(tileSizeY), offsetP.X+(j+1)*int(tileSizeX), offsetP.Y+(i+1)*int(tileSizeY)), i2, image.ZP, draw.Src) 135 | m.RequestPaint() 136 | } 137 | case FallbackTileProvider: 138 | tiles := t.RenderTileRange(c, d) 139 | usedFallback := t.UsedFallback() 140 | if len(tiles) > 0 { 141 | i2 := tiles[0].Img 142 | if i2 == nil { 143 | for _, k := range tiles { 144 | if k.Img != nil { 145 | i2 = k.Img 146 | break 147 | } 148 | } 149 | if i2 == nil { 150 | panic("Unable to find image") 151 | } 152 | } 153 | tileSizeX = float64(i2.Bounds().Dx()) 154 | tileLonWidth := tileSizeX / (cb.Lon - ca.Lon) 155 | offsetP.X = int(math.Floor((modA.Lon - a.Lon) * tileLonWidth)) 156 | tileSizeY = float64(i2.Bounds().Dy()) 157 | tileLatHeight := tileSizeY / (cb.Lat - ca.Lat) 158 | offsetP.Y = int(math.Floor((modA.Lat - a.Lat) * tileLatHeight)) 159 | // Now correct right/bottom to actual screen range 160 | //m.left.SetValueFloat64(a.Lon + tileLonWidth*float64(m.width.GetValueInt())) 161 | //m.bottom.SetValueFloat64(a.Lat + tileLatHeight*float64(m.height.GetValueInt())) 162 | } 163 | for _, k := range tiles { 164 | i = int(k.Tile.Y - c.Y) 165 | j = int(k.Tile.X - c.X) 166 | i2 := k.Img 167 | draw.Draw(img, image.Rect(offsetP.X+j*int(tileSizeX), offsetP.Y+i*int(tileSizeY), offsetP.X+(j+1)*int(tileSizeX), offsetP.Y+(i+1)*int(tileSizeY)), i2, image.ZP, draw.Src) 168 | m.RequestPaint() 169 | } 170 | if usedFallback { 171 | // There are fallback tiles present, so queue another render 172 | m.NeedsRender = true 173 | m.RequestPaint() 174 | } 175 | case AdvancedTileProvider: 176 | tiles := t.RenderTileRange(c, d) 177 | if len(tiles) > 0 { 178 | i2 := tiles[0].Img 179 | if i2 == nil { 180 | for _, k := range tiles { 181 | if k.Img != nil { 182 | i2 = k.Img 183 | break 184 | } 185 | } 186 | if i2 == nil { 187 | panic("Unable to find image") 188 | } 189 | } 190 | tileSizeX = float64(i2.Bounds().Dx()) 191 | tileLonWidth := tileSizeX / (cb.Lon - ca.Lon) 192 | offsetP.X = int(math.Floor((modA.Lon - a.Lon) * tileLonWidth)) 193 | tileSizeY = float64(i2.Bounds().Dy()) 194 | tileLatHeight := tileSizeY / (cb.Lat - ca.Lat) 195 | offsetP.Y = int(math.Floor((modA.Lat - a.Lat) * tileLatHeight)) 196 | // Now correct right/bottom to actual screen range 197 | //fmt.Printf("a.Lon %v modA.Lon %v ca.Lon %v cb.Lon %v tileLonWidth %v Expected right lon %v actual right lon %v offsetP.X %v\n", a.Lon, modA.Lon, ca.Lon, cb.Lon, tileLonWidth, a.Lon+float64(m.width.GetValueInt())/tileLonWidth, b.Lon, offsetP.X) 198 | //m.left.SetValueFloat64(a.Lon + float64(m.width.GetValueInt()-1)/tileLonWidth) 199 | //m.bottom.SetValueFloat64(a.Lat + float64(m.height.GetValueInt()-1)/tileLatHeight) 200 | } 201 | for _, k := range tiles { 202 | i = int(k.Tile.Y - c.Y) 203 | j = int(k.Tile.X - c.X) 204 | i2 := k.Img 205 | draw.Draw(img, image.Rect(offsetP.X+j*int(tileSizeX), offsetP.Y+i*int(tileSizeY), offsetP.X+(j+1)*int(tileSizeX), offsetP.Y+(i+1)*int(tileSizeY)), i2, image.ZP, draw.Src) 206 | m.RequestPaint() 207 | } 208 | case TileProvider: 209 | for i = 0; i < int(w); i++ { 210 | for j = 0; j < int(h); j++ { 211 | i2 := t.RenderTile(Tile{c.Z, c.Y + uint(i), c.X + uint(j)}) 212 | if i == 0 && j == 0 { 213 | tileSizeX = float64(i2.Bounds().Dx()) 214 | tileLonWidth := tileSizeX / (cb.Lon - ca.Lon) 215 | offsetP.X = int(math.Floor((modA.Lon - a.Lon) * tileLonWidth)) 216 | tileSizeY = float64(i2.Bounds().Dy()) 217 | tileLatHeight := tileSizeY / (cb.Lat - ca.Lat) 218 | offsetP.Y = int(math.Floor((modA.Lat - a.Lat) * tileLatHeight)) 219 | // Now correct right/bottom to actual screen range 220 | // m.left.SetValueFloat64(a.Lon + tileLonWidth*tileSizeX*float64(w)) 221 | // m.bottom.SetValueFloat64(a.Lat + tileLatHeight*tileSizeX*float64(h)) 222 | } 223 | draw.Draw(img, image.Rect(0, 0, int(tileSizeX), int(tileSizeY)), i2, image.Point{offsetP.X + j*int(tileSizeX), offsetP.Y + i*int(tileSizeY)}, draw.Src) 224 | m.RequestPaint() 225 | } 226 | } 227 | } 228 | m.Rendering = false 229 | } 230 | -------------------------------------------------------------------------------- /rendermodel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package renderview 6 | 7 | import ( 8 | "image" 9 | "math" 10 | "sync" 11 | ) 12 | 13 | // RenderModel is the interface you will implement to stand between your visualization code 14 | // and the RenderView. You are primarily responsible for providing a set of RenderParameters 15 | // and providing an Image upon request via Render. 16 | type RenderModel interface { 17 | Lock() 18 | Unlock() 19 | 20 | GetParameterNames() []string 21 | GetHintedParameterNames(hint int) []string 22 | GetHintedParameterNamesWithFallback(hint int) []string 23 | GetParameter(name string) RenderParameter 24 | Render() image.Image 25 | SetRequestPaintFunc(func()) 26 | GetRequestPaintFunc() func() 27 | } 28 | 29 | // EmptyRenderModel concretizes the most important elements of the RenderModel, the bag of Parameters (Params) 30 | // and the RequestPaint function (which the view sets) - call this latter function to inform the view 31 | // that you have provided a new image or set of information needing a render. It is not usable as a RenderModel 32 | // by itself, as the implementation of Render simply returns nil. Embed it in your RenderModel struct. 33 | type EmptyRenderModel struct { 34 | sync.Mutex 35 | 36 | Params []RenderParameter 37 | RequestPaint func() 38 | } 39 | 40 | // GetParameterNames returns a list of valid parameter names 41 | func (e *EmptyRenderModel) GetParameterNames() []string { 42 | s := make([]string, len(e.Params)) 43 | for i := 0; i < len(e.Params); i++ { 44 | s[i] = e.Params[i].GetName() 45 | } 46 | return s 47 | } 48 | 49 | // GetHintedParameterNames returns a list of parameters with one of the passed hints 50 | func (e *EmptyRenderModel) GetHintedParameterNames(hints int) []string { 51 | s := make([]string, 0, len(e.Params)) 52 | for i := 0; i < len(e.Params); i++ { 53 | if e.Params[i].GetHint()&hints > 0 { 54 | s = append(s, e.Params[i].GetName()) 55 | } 56 | } 57 | return s 58 | } 59 | 60 | // GetHintedParameterNamesWithFallback retrieves the names of parameters matching hints, 61 | // if that is the empty set, it retrieves the names of parameters with no hints 62 | func (e *EmptyRenderModel) GetHintedParameterNamesWithFallback(hints int) []string { 63 | s := make([]string, 0, len(e.Params)) 64 | for i := 0; i < len(e.Params); i++ { 65 | if e.Params[i].GetHint()&hints > 0 { 66 | s = append(s, e.Params[i].GetName()) 67 | } 68 | } 69 | if len(s) == 0 { 70 | for i := 0; i < len(e.Params); i++ { 71 | if e.Params[i].GetHint() == 0 { 72 | s = append(s, e.Params[i].GetName()) 73 | } 74 | } 75 | } 76 | return s 77 | } 78 | 79 | // GetParameter returns a named parameter. If you implement your own RenderModel from scratch, 80 | // without using the EmptyRenderModel as a basis, you must either include ALL the default 81 | // parameters, or duplicate the behavior of EmptyRenderModel in returning an EmptyParameter 82 | // when a non-existent parameter is requested. 83 | func (e *EmptyRenderModel) GetParameter(name string) RenderParameter { 84 | for _, p := range e.Params { 85 | if name == p.GetName() { 86 | return p 87 | } 88 | } 89 | 90 | return &EmptyParameter{} 91 | } 92 | 93 | func (e *EmptyRenderModel) Render() image.Image { 94 | return nil 95 | } 96 | 97 | // AddParameters accepts any number of parameters and adds them to the Params bag. 98 | // It does not do ANY checking for duplication! 99 | func (e *EmptyRenderModel) AddParameters(Params ...RenderParameter) { 100 | e.Params = append(e.Params, Params...) 101 | } 102 | 103 | // Included for completeness. In general, there is no need for your code to use 104 | // the RenderModel interface instead of a concrete form, so you can simply 105 | // access e.RequestPaint directly. 106 | func (e *EmptyRenderModel) GetRequestPaintFunc() func() { 107 | return e.RequestPaint 108 | } 109 | 110 | // Used by the RenderView to supply a function you can call to inform the view 111 | // that it should perform a repaint. 112 | func (e *EmptyRenderModel) SetRequestPaintFunc(f func()) { 113 | e.RequestPaint = f 114 | } 115 | 116 | /* 117 | // EmptyRenderModel is not functional by itself 118 | func NewEmptyRenderModel() *EmptyRenderModel { 119 | return &EmptyRenderModel{ 120 | Params: make([]RenderParameter, 0, 10), 121 | } 122 | }*/ 123 | 124 | // InitializeEmptyRenderModel should be called to initialize the EmptyRenderModel 125 | // when embedded in your own struct. 126 | func InitializeEmptyRenderModel(m *EmptyRenderModel) { 127 | m.Params = make([]RenderParameter, 0, 10) 128 | } 129 | 130 | // BasicRenderModel should suffice for many users, and can be embedded to provide its 131 | // functionality to your own models. It provides an easy way to attach your own 132 | // rendering implementation that will be called in a separate goroutine. 133 | type BasicRenderModel struct { 134 | EmptyRenderModel 135 | 136 | RequestRender chan interface{} 137 | NeedsRender bool 138 | Rendering bool 139 | Img image.Image 140 | 141 | started bool 142 | 143 | InnerRender func() 144 | } 145 | 146 | // Called by RenderView 147 | func (m *BasicRenderModel) Render() image.Image { 148 | m.Lock() 149 | defer m.Unlock() 150 | rendering := m.Rendering 151 | if !rendering { 152 | m.RequestRender <- true 153 | m.NeedsRender = false 154 | } else { 155 | m.NeedsRender = true 156 | } 157 | return m.Img 158 | } 159 | 160 | // GoRender is called by Start and calls your provided InnerRender function when needed. 161 | func (m *BasicRenderModel) GoRender() { 162 | for { 163 | select { 164 | case <-m.RequestRender: 165 | if !(m.InnerRender == nil) { 166 | m.InnerRender() 167 | if m.NeedsRender { 168 | m.NeedsRender = false 169 | m.InnerRender() 170 | } 171 | } 172 | } 173 | } 174 | } 175 | 176 | // Start only needs to be called if you have embedded BasicRenderModel in your own struct. 177 | func (m *BasicRenderModel) Start() { 178 | if !m.started { 179 | m.started = true 180 | go m.GoRender() 181 | } 182 | } 183 | 184 | func NewBasicRenderModel() *BasicRenderModel { 185 | m := BasicRenderModel{ 186 | EmptyRenderModel: EmptyRenderModel{ 187 | Params: make([]RenderParameter, 0, 10), 188 | }, 189 | RequestRender: make(chan interface{}, 10), 190 | } 191 | m.started = true 192 | go m.GoRender() 193 | return &m 194 | } 195 | 196 | // Use Initialize to set up a BasicRenderModel when you have embedded it in 197 | // your own model 198 | // Remember to add a go m.GoRender() or call Start() 199 | func InitializeBasicRenderModel(m *BasicRenderModel) { 200 | m.Params = make([]RenderParameter, 0, 10) 201 | m.RequestRender = make(chan interface{}, 10) 202 | } 203 | 204 | func DefaultParameters(useint bool, hint int, options int, left float64, top float64, right float64, bottom float64) []RenderParameter { 205 | if useint { 206 | return SetHints(hint, 207 | NewIntRP("left", int(math.Floor(left))), 208 | NewIntRP("top", int(math.Floor(top))), 209 | NewIntRP("right", int(math.Floor(right))), 210 | NewIntRP("bottom", int(math.Floor(bottom))), 211 | NewIntRP("width", 100), 212 | NewIntRP("height", 100), 213 | NewIntRP("options", options)) 214 | } else { 215 | return SetHints(hint, 216 | NewFloat64RP("left", left), 217 | NewFloat64RP("top", top), 218 | NewFloat64RP("right", right), 219 | NewFloat64RP("bottom", bottom), 220 | NewIntRP("width", 100), 221 | NewIntRP("height", 100), 222 | NewIntRP("options", options)) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /renderparameter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package renderview 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | HINT_NONE = 1 << iota 16 | HINT_HIDE = 1 << iota 17 | HINT_SIDEBAR = 1 << iota 18 | HINT_FOOTER = 1 << iota 19 | HINT_FULLTEXT = 1 << iota 20 | ) 21 | 22 | type RenderParameter interface { 23 | GetName() string 24 | GetType() string 25 | GetHint() int 26 | SetHint(int) 27 | GetValueInt() int 28 | GetValueUInt32() uint32 29 | GetValueFloat64() float64 30 | GetValueComplex128() complex128 31 | GetValueString() string 32 | SetValueInt(value int) int 33 | SetValueUInt32(value uint32) uint32 34 | SetValueFloat64(value float64) float64 35 | SetValueComplex128(value complex128) complex128 36 | SetValueString(value string) string 37 | } 38 | 39 | type EmptyParameter struct { 40 | Name string 41 | Type string 42 | Hint int 43 | } 44 | 45 | func (e *EmptyParameter) GetName() string { 46 | return e.Name 47 | } 48 | 49 | func (e *EmptyParameter) GetType() string { 50 | return e.Type 51 | } 52 | 53 | func (e *EmptyParameter) GetHint() int { 54 | return e.Hint 55 | } 56 | 57 | func (e *EmptyParameter) GetValueInt() int { 58 | return 0 59 | } 60 | 61 | func (e *EmptyParameter) GetValueUInt32() uint32 { 62 | return 0 63 | } 64 | func (e *EmptyParameter) GetValueFloat64() float64 { 65 | return 0 66 | } 67 | func (e *EmptyParameter) GetValueString() string { 68 | return "" 69 | } 70 | 71 | func (e *EmptyParameter) GetValueComplex128() complex128 { 72 | return 0 73 | } 74 | 75 | func (e *EmptyParameter) SetHint(value int) { 76 | e.Hint = value 77 | } 78 | func (e *EmptyParameter) SetValueInt(value int) int { 79 | return 0 80 | } 81 | func (e *EmptyParameter) SetValueUInt32(value uint32) uint32 { 82 | return 0 83 | } 84 | func (e *EmptyParameter) SetValueFloat64(value float64) float64 { 85 | return 0 86 | } 87 | func (e *EmptyParameter) SetValueString(value string) string { 88 | return "" 89 | } 90 | func (e *EmptyParameter) SetValueComplex128(value complex128) complex128 { 91 | return 0 92 | } 93 | 94 | type UInt32RenderParameter struct { 95 | EmptyParameter 96 | 97 | Value uint32 98 | } 99 | 100 | func (e *UInt32RenderParameter) GetValueUInt32() uint32 { 101 | return e.Value 102 | } 103 | 104 | func (e *UInt32RenderParameter) SetValueUInt32(v uint32) uint32 { 105 | e.Value = v 106 | return e.Value 107 | } 108 | 109 | func (e *UInt32RenderParameter) GetValueString() string { 110 | return fmt.Sprintf("%v", e.Value) 111 | } 112 | 113 | func (e *UInt32RenderParameter) SetValueString(v string) string { 114 | r, err := strconv.ParseInt(v, 10, 32) 115 | if err != nil { 116 | e.SetValueUInt32(uint32(r)) 117 | } else { 118 | e.SetValueUInt32(0) 119 | } 120 | return e.GetValueString() 121 | } 122 | 123 | type IntRenderParameter struct { 124 | EmptyParameter 125 | 126 | Value int 127 | } 128 | 129 | func (e *IntRenderParameter) GetValueInt() int { 130 | return e.Value 131 | } 132 | 133 | func (e *IntRenderParameter) SetValueInt(v int) int { 134 | e.Value = v 135 | return e.Value 136 | } 137 | 138 | type Float64RenderParameter struct { 139 | EmptyParameter 140 | 141 | Value float64 142 | } 143 | 144 | func (e *Float64RenderParameter) GetValueFloat64() float64 { 145 | return e.Value 146 | } 147 | 148 | func (e *Float64RenderParameter) SetValueFloat64(v float64) float64 { 149 | e.Value = v 150 | return e.Value 151 | } 152 | 153 | type Complex128RenderParameter struct { 154 | EmptyParameter 155 | 156 | Value complex128 157 | } 158 | 159 | func (e *Complex128RenderParameter) GetValueComplex128() complex128 { 160 | return e.Value 161 | } 162 | 163 | func (e *Complex128RenderParameter) SetValueComplex128(v complex128) complex128 { 164 | e.Value = v 165 | return e.Value 166 | } 167 | 168 | type StringRenderParameter struct { 169 | EmptyParameter 170 | 171 | Value string 172 | } 173 | 174 | func (e *StringRenderParameter) GetValueString() string { 175 | return e.Value 176 | } 177 | 178 | func (e *StringRenderParameter) SetValueString(v string) string { 179 | e.Value = v 180 | return e.Value 181 | } 182 | 183 | func NewUInt32RP(name string, value uint32) *UInt32RenderParameter { 184 | return &UInt32RenderParameter{ 185 | EmptyParameter: EmptyParameter{ 186 | Name: name, 187 | Type: "uint32", 188 | }, 189 | Value: value, 190 | } 191 | } 192 | 193 | func NewIntRP(name string, value int) *IntRenderParameter { 194 | return &IntRenderParameter{ 195 | EmptyParameter: EmptyParameter{ 196 | Name: name, 197 | Type: "int", 198 | }, 199 | Value: value, 200 | } 201 | } 202 | 203 | func NewFloat64RP(name string, value float64) *Float64RenderParameter { 204 | return &Float64RenderParameter{ 205 | EmptyParameter: EmptyParameter{ 206 | Name: name, 207 | Type: "float64", 208 | }, 209 | Value: value, 210 | } 211 | } 212 | 213 | func NewComplex128RP(name string, value complex128) *Complex128RenderParameter { 214 | return &Complex128RenderParameter{ 215 | EmptyParameter: EmptyParameter{ 216 | Name: name, 217 | Type: "complex128", 218 | }, 219 | Value: value, 220 | } 221 | } 222 | 223 | func NewStringRP(name string, value string) *StringRenderParameter { 224 | return &StringRenderParameter{ 225 | EmptyParameter: EmptyParameter{ 226 | Name: name, 227 | Type: "string", 228 | }, 229 | Value: value, 230 | } 231 | } 232 | 233 | // Utility functions 234 | 235 | // GetParameterValueAsString replaces the need to implement GetValueString on 236 | // each parameter. Custom parameters should override GetValueString to override 237 | // this behavior. 238 | func GetParameterValueAsString(p RenderParameter) string { 239 | switch p.GetType() { 240 | case "int": 241 | return fmt.Sprintf("%v", p.GetValueInt()) 242 | case "uint32": 243 | return fmt.Sprintf("%v", p.GetValueUInt32()) 244 | case "float64": 245 | return fmt.Sprintf("%v", p.GetValueFloat64()) 246 | case "complex128": 247 | return fmt.Sprintf("%v", p.GetValueComplex128()) 248 | case "string": 249 | return p.GetValueString() 250 | default: 251 | return p.GetValueString() 252 | } 253 | } 254 | 255 | func ParseComplex(v string) (complex128, error) { 256 | v = strings.Replace(v, ",", "+", -1) 257 | l := strings.Split(v, "+") 258 | r, err := strconv.ParseFloat(l[0], 64) 259 | if err != nil { 260 | return 0, err 261 | } 262 | if len(l) > 1 { 263 | l[1] = strings.Replace(l[1], "i", "", -1) 264 | i, err := strconv.ParseFloat(l[1], 64) 265 | if err != nil { 266 | return 0, err 267 | } 268 | return complex(r, i), nil 269 | } else { 270 | return complex(r, 0), nil 271 | } 272 | 273 | } 274 | 275 | // SetParameterValueAsString replaces the need to implement SetValueString on 276 | // each parameter. Custom parameters should override SetValueString to override 277 | // this behavior. 278 | func SetParameterValueFromString(p RenderParameter, v string) { 279 | switch p.GetType() { 280 | case "int": 281 | i, err := strconv.Atoi(v) 282 | if err == nil { 283 | p.SetValueInt(i) 284 | } 285 | case "uint32": 286 | i, err := strconv.ParseInt(v, 10, 32) 287 | if err == nil { 288 | p.SetValueUInt32(uint32(i)) 289 | } 290 | case "float64": 291 | f, err := strconv.ParseFloat(v, 64) 292 | if err == nil { 293 | p.SetValueFloat64(f) 294 | } 295 | case "complex128": 296 | c, err := ParseComplex(v) 297 | if err == nil { 298 | p.SetValueComplex128(c) 299 | } 300 | case "string": 301 | p.SetValueString(v) 302 | default: 303 | p.SetValueString(v) 304 | 305 | } 306 | } 307 | 308 | func SetHints(hint int, params ...RenderParameter) []RenderParameter { 309 | for _, p := range params { 310 | p.SetHint(hint) 311 | } 312 | 313 | return params 314 | } 315 | 316 | func GetParameterStatusString(params ...RenderParameter) string { 317 | buffer := bytes.NewBuffer(make([]byte, 0, len(params)*15)) 318 | for i, p := range params { 319 | is := strconv.Itoa(i) 320 | buffer.WriteString(is) 321 | buffer.WriteString(":") 322 | buffer.WriteString(GetParameterValueAsString(p)) 323 | buffer.WriteString(",") 324 | } 325 | return buffer.String() 326 | } 327 | -------------------------------------------------------------------------------- /renderview.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Howard C. Shaw III. All rights reserved. 2 | // Use of this source code is governed by the MIT-license 3 | // as defined in the LICENSE file. 4 | 5 | package renderview 6 | 7 | const ( 8 | OPT_NONE = iota // 0 9 | OPT_CENTER_ZOOM = 1 << iota // 1 10 | OPT_AUTO_ZOOM = 1 << iota // 2 11 | ) 12 | 13 | const ZOOM_RATE = 0.1 14 | --------------------------------------------------------------------------------