• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fyne-io / fyne / 18807176232

25 Oct 2025 06:34PM UTC coverage: 61.057% (-0.004%) from 61.061%
18807176232

Pull #5989

github

Jacalz
Fix TODO regarding comparable map key
Pull Request #5989: RFC: Proof of concept for upgrading Go to 1.24

155 of 188 new or added lines in 62 files covered. (82.45%)

27 existing lines in 6 files now uncovered.

25609 of 41943 relevant lines covered (61.06%)

692.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

6.58
/internal/painter/draw.go
1
package painter
2

3
import (
4
        "image"
5
        "image/color"
6
        "math"
7

8
        "fyne.io/fyne/v2"
9
        "fyne.io/fyne/v2/canvas"
10

11
        "github.com/srwiley/rasterx"
12
        "golang.org/x/image/math/fixed"
13
)
14

15
const quarterCircleControl = 1 - 0.55228
16

17
// DrawArc rasterizes the given arc object into an image.
18
// The scale function is used to understand how many pixels are required per unit of size.
19
// The arc is drawn from StartAngle to EndAngle (in degrees).
20
// 0°/360 is top, 90° is right, 180° is bottom, 270° is left
21
// 0°/-360 is top, -90° is left, -180° is bottom, -270° is right
22
func DrawArc(arc *canvas.Arc, vectorPad float32, scale func(float32) float32) *image.RGBA {
×
23
        size := arc.Size()
×
24

×
25
        width := int(scale(size.Width + vectorPad*2))
×
26
        height := int(scale(size.Height + vectorPad*2))
×
27

×
28
        raw := image.NewRGBA(image.Rect(0, 0, width, height))
×
29
        scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds())
×
30

×
31
        centerX := float64(width) / 2
×
32
        centerY := float64(height) / 2
×
33

×
NEW
34
        outerRadius := min(size.Width, size.Height) / 2
×
NEW
35
        innerRadius := outerRadius * min(1.0, max(0.0, arc.CutoutRatio))
×
NEW
36
        cornerRadius := min(GetMaximumRadiusArc(outerRadius, innerRadius, arc.EndAngle-arc.StartAngle), arc.CornerRadius)
×
37
        startAngle, endAngle := NormalizeArcAngles(arc.StartAngle, arc.EndAngle)
×
38

×
39
        // convert to radians
×
40
        startRad := float64(startAngle * math.Pi / 180.0)
×
41
        endRad := float64(endAngle * math.Pi / 180.0)
×
42
        sweep := endRad - startRad
×
43
        if sweep == 0 {
×
44
                // nothing to draw
×
45
                return raw
×
46
        }
×
47

48
        if sweep > 2*math.Pi {
×
49
                sweep = 2 * math.Pi
×
50
        } else if sweep < -2*math.Pi {
×
51
                sweep = -2 * math.Pi
×
52
        }
×
53

54
        cornerRadius = scale(cornerRadius)
×
55
        outerRadius = scale(outerRadius)
×
56
        innerRadius = scale(innerRadius)
×
57

×
58
        if arc.FillColor != nil {
×
59
                filler := rasterx.NewFiller(width, height, scanner)
×
60
                filler.SetColor(arc.FillColor)
×
61
                // rasterx.AddArc is not used because it does not support rounded corners
×
62
                drawRoundArc(filler, centerX, centerY, float64(outerRadius), float64(innerRadius), startRad, sweep, float64(cornerRadius))
×
63
                filler.Draw()
×
64
        }
×
65

66
        stroke := float64(scale(arc.StrokeWidth))
×
67
        if arc.StrokeColor != nil && stroke > 0 {
×
68
                dasher := rasterx.NewDasher(width, height, scanner)
×
69
                dasher.SetColor(arc.StrokeColor)
×
70
                dasher.SetStroke(fixed.Int26_6(stroke*64), 0, nil, nil, nil, 0, nil, 0)
×
71
                // rasterx.AddArc is not used because it does not support rounded corners
×
72
                drawRoundArc(dasher, centerX, centerY, float64(outerRadius), float64(innerRadius), startRad, sweep, float64(cornerRadius))
×
73
                dasher.Draw()
×
74
        }
×
75

76
        return raw
×
77
}
78

79
// DrawCircle rasterizes the given circle object into an image.
80
// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges.
81
// The scale function is used to understand how many pixels are required per unit of size.
82
func DrawCircle(circle *canvas.Circle, vectorPad float32, scale func(float32) float32) *image.RGBA {
×
83
        size := circle.Size()
×
84
        radius := GetMaximumRadius(size)
×
85

×
86
        width := int(scale(size.Width + vectorPad*2))
×
87
        height := int(scale(size.Height + vectorPad*2))
×
88
        stroke := scale(circle.StrokeWidth)
×
89

×
90
        raw := image.NewRGBA(image.Rect(0, 0, width, height))
×
91
        scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds())
×
92

×
93
        if circle.FillColor != nil {
×
94
                filler := rasterx.NewFiller(width, height, scanner)
×
95
                filler.SetColor(circle.FillColor)
×
96
                rasterx.AddCircle(float64(width/2), float64(height/2), float64(scale(radius)), filler)
×
97
                filler.Draw()
×
98
        }
×
99

100
        dasher := rasterx.NewDasher(width, height, scanner)
×
101
        dasher.SetColor(circle.StrokeColor)
×
102
        dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0)
×
103
        rasterx.AddCircle(float64(width/2), float64(height/2), float64(scale(radius)), dasher)
×
104
        dasher.Draw()
×
105

×
106
        return raw
×
107
}
108

109
// DrawLine rasterizes the given line object into an image.
110
// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges.
111
// The scale function is used to understand how many pixels are required per unit of size.
112
func DrawLine(line *canvas.Line, vectorPad float32, scale func(float32) float32) *image.RGBA {
×
113
        col := line.StrokeColor
×
114
        size := line.Size()
×
115
        width := int(scale(size.Width + vectorPad*2))
×
116
        height := int(scale(size.Height + vectorPad*2))
×
117
        stroke := scale(line.StrokeWidth)
×
118
        if stroke < 1 { // software painter doesn't fade lines to compensate
×
119
                stroke = 1
×
120
        }
×
121

122
        raw := image.NewRGBA(image.Rect(0, 0, width, height))
×
123
        scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds())
×
124
        dasher := rasterx.NewDasher(width, height, scanner)
×
125
        dasher.SetColor(col)
×
126
        dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0)
×
127
        position := line.Position()
×
128
        p1x, p1y := scale(line.Position1.X-position.X+vectorPad), scale(line.Position1.Y-position.Y+vectorPad)
×
129
        p2x, p2y := scale(line.Position2.X-position.X+vectorPad), scale(line.Position2.Y-position.Y+vectorPad)
×
130

×
131
        if stroke <= 1.5 { // adjust to support 1px
×
132
                if p1x == p2x {
×
133
                        p1x -= 0.5
×
134
                        p2x -= 0.5
×
135
                }
×
136
                if p1y == p2y {
×
137
                        p1y -= 0.5
×
138
                        p2y -= 0.5
×
139
                }
×
140
        }
141

142
        dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y)))
×
143
        dasher.Line(rasterx.ToFixedP(float64(p2x), float64(p2y)))
×
144
        dasher.Stop(true)
×
145
        dasher.Draw()
×
146

×
147
        return raw
×
148
}
149

150
// DrawPolygon rasterizes the given regular polygon object into an image.
151
// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges.
152
// The scale function is used to understand how many pixels are required per unit of size.
153
func DrawPolygon(polygon *canvas.Polygon, vectorPad float32, scale func(float32) float32) *image.RGBA {
×
154
        size := polygon.Size()
×
155

×
156
        width := int(scale(size.Width + vectorPad*2))
×
157
        height := int(scale(size.Height + vectorPad*2))
×
NEW
158
        outerRadius := scale(min(size.Width, size.Height) / 2)
×
NEW
159
        cornerRadius := scale(min(GetMaximumRadius(size), polygon.CornerRadius))
×
160
        sides := int(polygon.Sides)
×
161
        angle := polygon.Angle
×
162

×
163
        raw := image.NewRGBA(image.Rect(0, 0, width, height))
×
164
        scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds())
×
165

×
166
        if polygon.FillColor != nil {
×
167
                filler := rasterx.NewFiller(width, height, scanner)
×
168
                filler.SetColor(polygon.FillColor)
×
169
                drawRegularPolygon(float64(width/2), float64(height/2), float64(outerRadius), float64(cornerRadius), float64(angle), int(sides), filler)
×
170
                filler.Draw()
×
171
        }
×
172

173
        if polygon.StrokeColor != nil && polygon.StrokeWidth > 0 {
×
174
                dasher := rasterx.NewDasher(width, height, scanner)
×
175
                dasher.SetColor(polygon.StrokeColor)
×
176
                dasher.SetStroke(fixed.Int26_6(float64(scale(polygon.StrokeWidth))*64), 0, nil, nil, nil, 0, nil, 0)
×
177
                drawRegularPolygon(float64(width/2), float64(height/2), float64(outerRadius), float64(cornerRadius), float64(angle), int(sides), dasher)
×
178
                dasher.Draw()
×
179
        }
×
180

181
        return raw
×
182
}
183

184
// DrawRectangle rasterizes the given rectangle object with stroke border into an image.
185
// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges.
186
// The scale function is used to understand how many pixels are required per unit of size.
187
func DrawRectangle(rect *canvas.Rectangle, rWidth, rHeight, vectorPad float32, scale func(float32) float32) *image.RGBA {
×
188
        topRightRadius := GetCornerRadius(rect.TopRightCornerRadius, rect.CornerRadius)
×
189
        topLeftRadius := GetCornerRadius(rect.TopLeftCornerRadius, rect.CornerRadius)
×
190
        bottomRightRadius := GetCornerRadius(rect.BottomRightCornerRadius, rect.CornerRadius)
×
191
        bottomLeftRadius := GetCornerRadius(rect.BottomLeftCornerRadius, rect.CornerRadius)
×
192
        return drawOblong(rect.FillColor, rect.StrokeColor, rect.StrokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, rWidth, rHeight, vectorPad, scale)
×
193
}
×
194

195
func drawOblong(fill, strokeCol color.Color, strokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, rWidth, rHeight, vectorPad float32, scale func(float32) float32) *image.RGBA {
×
196
        // The maximum possible corner radii for a circular shape
×
197
        size := fyne.NewSize(rWidth, rHeight)
×
198
        topRightRadius = GetMaximumCornerRadius(topRightRadius, topLeftRadius, bottomRightRadius, size)
×
199
        topLeftRadius = GetMaximumCornerRadius(topLeftRadius, topRightRadius, bottomLeftRadius, size)
×
200
        bottomRightRadius = GetMaximumCornerRadius(bottomRightRadius, bottomLeftRadius, topRightRadius, size)
×
201
        bottomLeftRadius = GetMaximumCornerRadius(bottomLeftRadius, bottomRightRadius, topLeftRadius, size)
×
202

×
203
        width := int(scale(rWidth + vectorPad*2))
×
204
        height := int(scale(rHeight + vectorPad*2))
×
205
        stroke := scale(strokeWidth)
×
206

×
207
        raw := image.NewRGBA(image.Rect(0, 0, width, height))
×
208
        scanner := rasterx.NewScannerGV(int(rWidth), int(rHeight), raw, raw.Bounds())
×
209

×
210
        scaledPad := scale(vectorPad)
×
211
        p1x, p1y := scaledPad, scaledPad
×
212
        p2x, p2y := scale(rWidth)+scaledPad, scaledPad
×
213
        p3x, p3y := scale(rWidth)+scaledPad, scale(rHeight)+scaledPad
×
214
        p4x, p4y := scaledPad, scale(rHeight)+scaledPad
×
215

×
216
        if fill != nil {
×
217
                filler := rasterx.NewFiller(width, height, scanner)
×
218
                filler.SetColor(fill)
×
219
                if topRightRadius == topLeftRadius && bottomRightRadius == bottomLeftRadius && topRightRadius == bottomRightRadius {
×
220
                        // If all corners are the same, we can draw a simple rectangle
×
221
                        radius := topRightRadius
×
222
                        if radius == 0 {
×
223
                                rasterx.AddRect(float64(p1x), float64(p1y), float64(p3x), float64(p3y), 0, filler)
×
224
                        } else {
×
225
                                r := float64(scale(radius))
×
226
                                rasterx.AddRoundRect(float64(p1x), float64(p1y), float64(p3x), float64(p3y), r, r, 0, rasterx.RoundGap, filler)
×
227
                        }
×
228
                } else {
×
229
                        rTL, rTR, rBR, rBL := scale(topLeftRadius), scale(topRightRadius), scale(bottomRightRadius), scale(bottomLeftRadius)
×
230
                        // Top-left corner
×
231
                        c := quarterCircleControl * rTL
×
232
                        if c != 0 {
×
233
                                filler.Start(rasterx.ToFixedP(float64(p1x), float64(p1y+rTL)))
×
234
                                filler.CubeBezier(rasterx.ToFixedP(float64(p1x), float64(p1y+c)), rasterx.ToFixedP(float64(p1x+c), float64(p1y)), rasterx.ToFixedP(float64(p1x+rTL), float64(p1y)))
×
235
                        } else {
×
236
                                filler.Start(rasterx.ToFixedP(float64(p1x), float64(p1y)))
×
237
                        }
×
238
                        // Top edge to top-right
239
                        c = quarterCircleControl * rTR
×
240
                        filler.Line(rasterx.ToFixedP(float64(p2x-rTR), float64(p2y)))
×
241
                        if c != 0 {
×
242
                                filler.CubeBezier(rasterx.ToFixedP(float64(p2x-c), float64(p2y)), rasterx.ToFixedP(float64(p2x), float64(p2y+c)), rasterx.ToFixedP(float64(p2x), float64(p2y+rTR)))
×
243
                        }
×
244
                        // Right edge to bottom-right
245
                        c = quarterCircleControl * rBR
×
246
                        filler.Line(rasterx.ToFixedP(float64(p3x), float64(p3y-rBR)))
×
247
                        if c != 0 {
×
248
                                filler.CubeBezier(rasterx.ToFixedP(float64(p3x), float64(p3y-c)), rasterx.ToFixedP(float64(p3x-c), float64(p3y)), rasterx.ToFixedP(float64(p3x-rBR), float64(p3y)))
×
249
                        }
×
250
                        // Bottom edge to bottom-left
251
                        c = quarterCircleControl * rBL
×
252
                        filler.Line(rasterx.ToFixedP(float64(p4x+rBL), float64(p4y)))
×
253
                        if c != 0 {
×
254
                                filler.CubeBezier(rasterx.ToFixedP(float64(p4x+c), float64(p4y)), rasterx.ToFixedP(float64(p4x), float64(p4y-c)), rasterx.ToFixedP(float64(p4x), float64(p4y-rBL)))
×
255
                        }
×
256
                        // Left edge to top-left
257
                        filler.Line(rasterx.ToFixedP(float64(p1x), float64(p1y+rTL)))
×
258
                        filler.Stop(true)
×
259
                }
260
                filler.Draw()
×
261
        }
262

263
        if strokeCol != nil && strokeWidth > 0 {
×
264
                dasher := rasterx.NewDasher(width, height, scanner)
×
265
                dasher.SetColor(strokeCol)
×
266
                dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0)
×
267
                rTL, rTR, rBR, rBL := scale(topLeftRadius), scale(topRightRadius), scale(bottomRightRadius), scale(bottomLeftRadius)
×
268
                c := quarterCircleControl * rTL
×
269
                if c != 0 {
×
270
                        dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y+rTL)))
×
271
                        dasher.CubeBezier(rasterx.ToFixedP(float64(p1x), float64(p1y+c)), rasterx.ToFixedP(float64(p1x+c), float64(p1y)), rasterx.ToFixedP(float64(p1x+rTL), float64(p2y)))
×
272
                } else {
×
273
                        dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y)))
×
274
                }
×
275
                c = quarterCircleControl * rTR
×
276
                dasher.Line(rasterx.ToFixedP(float64(p2x-rTR), float64(p2y)))
×
277
                if c != 0 {
×
278
                        dasher.CubeBezier(rasterx.ToFixedP(float64(p2x-c), float64(p2y)), rasterx.ToFixedP(float64(p2x), float64(p2y+c)), rasterx.ToFixedP(float64(p2x), float64(p2y+rTR)))
×
279
                }
×
280
                c = quarterCircleControl * rBR
×
281
                dasher.Line(rasterx.ToFixedP(float64(p3x), float64(p3y-rBR)))
×
282
                if c != 0 {
×
283
                        dasher.CubeBezier(rasterx.ToFixedP(float64(p3x), float64(p3y-c)), rasterx.ToFixedP(float64(p3x-c), float64(p3y)), rasterx.ToFixedP(float64(p3x-rBR), float64(p3y)))
×
284
                }
×
285
                c = quarterCircleControl * rBL
×
286
                dasher.Line(rasterx.ToFixedP(float64(p4x+rBL), float64(p4y)))
×
287
                if c != 0 {
×
288
                        dasher.CubeBezier(rasterx.ToFixedP(float64(p4x+c), float64(p4y)), rasterx.ToFixedP(float64(p4x), float64(p4y-c)), rasterx.ToFixedP(float64(p4x), float64(p4y-rBL)))
×
289
                }
×
290
                dasher.Stop(true)
×
291
                dasher.Draw()
×
292
        }
293

294
        return raw
×
295
}
296

297
// drawRegularPolygon draws a regular n-sides centered at (cx,cy) with
298
// radius, rounded corners of cornerRadius, rotated by rot degrees.
299
func drawRegularPolygon(cx, cy, radius, cornerRadius, rot float64, sides int, p rasterx.Adder) {
×
300
        if sides < 3 || radius <= 0 {
×
301
                return
×
302
        }
×
303
        gf := rasterx.RoundGap
×
304
        angleStep := 2 * math.Pi / float64(sides)
×
305
        rotRads := rot*math.Pi/180 - math.Pi/2
×
306

×
307
        // fully rounded, draw circle
×
308
        if math.Min(cornerRadius, radius) == radius {
×
309
                rasterx.AddCircle(cx, cy, radius, p)
×
310
                return
×
311
        }
×
312

313
        // sharp polygon fast path
314
        if cornerRadius <= 0 {
×
315
                x0 := cx + radius*math.Cos(rotRads)
×
316
                y0 := cy + radius*math.Sin(rotRads)
×
317
                p.Start(rasterx.ToFixedP(x0, y0))
×
318
                for i := 1; i < sides; i++ {
×
319
                        t := rotRads + angleStep*float64(i)
×
320
                        p.Line(rasterx.ToFixedP(cx+radius*math.Cos(t), cy+radius*math.Sin(t)))
×
321
                }
×
322
                p.Stop(true)
×
323
                return
×
324
        }
325

326
        norm := func(x, y float64) (nx, ny float64) {
×
327
                l := math.Hypot(x, y)
×
328
                if l == 0 {
×
329
                        return 0, 0
×
330
                }
×
331
                return x / l, y / l
×
332
        }
333

334
        // regular polygon vertices
335
        xs := make([]float64, sides)
×
336
        ys := make([]float64, sides)
×
NEW
337
        for i := range sides {
×
338
                t := rotRads + angleStep*float64(i)
×
339
                xs[i] = cx + radius*math.Cos(t)
×
340
                ys[i] = cy + radius*math.Sin(t)
×
341
        }
×
342

343
        // interior angle and side length
344
        alpha := math.Pi * (float64(sides) - 2) / float64(sides)
×
345
        r := cornerRadius
×
346

×
347
        // distances for tangency and center placement
×
348
        tTrim := r / math.Tan(alpha/2) // along each edge from vertex to tangency
×
349
        d := r / math.Sin(alpha/2)     // from vertex to arc center along interior bisector
×
350

×
351
        // precompute fillet geometry per vertex
×
352
        type pt struct{ x, y float64 }
×
353
        sPts := make([]pt, sides) // start tangency (on incoming edge)
×
354
        vS := make([]pt, sides)   // center->start vector
×
355
        vE := make([]pt, sides)   // center->end vector
×
356
        cPts := make([]pt, sides) // arc centers
×
357

×
NEW
358
        for i := range sides {
×
359
                prv := (i - 1 + sides) % sides
×
360
                nxt := (i + 1) % sides
×
361

×
362
                // unit directions
×
363
                uInX, uInY := xs[i]-xs[prv], ys[i]-ys[prv]   // prev -> i
×
364
                uOutX, uOutY := xs[nxt]-xs[i], ys[nxt]-ys[i] // i -> next
×
365
                uInX, uInY = norm(uInX, uInY)
×
366
                uOutX, uOutY = norm(uOutX, uOutY)
×
367

×
368
                // tangency points along edges from the vertex
×
369
                sx, sy := xs[i]-uInX*tTrim, ys[i]-uInY*tTrim   // incoming (toward prev)
×
370
                ex, ey := xs[i]+uOutX*tTrim, ys[i]+uOutY*tTrim // outgoing (toward next)
×
371

×
372
                // interior bisector direction and arc center
×
373
                bx, by := -uInX+uOutX, -uInY+uOutY
×
374
                bx, by = norm(bx, by)
×
375
                cxI, cyI := xs[i]+bx*d, ys[i]+by*d
×
376

×
377
                // center->tangent vectors
×
378
                vsx, vsy := sx-cxI, sy-cyI
×
379
                velx, vely := ex-cxI, ey-cyI
×
380

×
381
                sPts[i] = pt{sx, sy}
×
382
                vS[i] = pt{vsx, vsy}
×
383
                vE[i] = pt{velx, vely}
×
384
                cPts[i] = pt{cxI, cyI}
×
385
        }
×
386

387
        // start at s0, arc corner 0, then line+arc around, close last edge
388
        p.Start(rasterx.ToFixedP(sPts[0].x, sPts[0].y))
×
389
        gf(p,
×
390
                rasterx.ToFixedP(cPts[0].x, cPts[0].y),
×
391
                rasterx.ToFixedP(vS[0].x, vS[0].y),
×
392
                rasterx.ToFixedP(vE[0].x, vE[0].y),
×
393
        )
×
394
        for i := 1; i < sides; i++ {
×
395
                p.Line(rasterx.ToFixedP(sPts[i].x, sPts[i].y))
×
396
                gf(p,
×
397
                        rasterx.ToFixedP(cPts[i].x, cPts[i].y),
×
398
                        rasterx.ToFixedP(vS[i].x, vS[i].y),
×
399
                        rasterx.ToFixedP(vE[i].x, vE[i].y),
×
400
                )
×
401
        }
×
402
        p.Line(rasterx.ToFixedP(sPts[0].x, sPts[0].y))
×
403
        p.Stop(true)
×
404
}
405

406
// drawRoundArc constructs a rounded pie slice or annular sector
407
// it uses the Unit circle coordinate system
408
func drawRoundArc(adder rasterx.Adder, cx, cy, outer, inner, start, sweep, cr float64) {
×
409
        if sweep == 0 {
×
410
                return
×
411
        }
×
412

413
        cosSinPoint := func(cx, cy, r, ang float64) (x, y float64) {
×
414
                return cx + r*math.Cos(ang), cy - r*math.Sin(ang)
×
415
        }
×
416

417
        // addCircularArc appends a circular arc to the current path using cubic Bezier approximation.
418
        // 'adder' must already be positioned at the arc start point.
419
        // sweep is signed (positive = CCW, negative = CW).
420
        addCircularArc := func(adder rasterx.Adder, cx, cy, r, start, sweep float64) {
×
421
                if sweep == 0 || r == 0 {
×
422
                        return
×
423
                }
×
424
                segCount := int(math.Ceil(math.Abs(sweep) / (math.Pi / 2.0)))
×
425
                da := sweep / float64(segCount)
×
426

×
NEW
427
                for i := range segCount {
×
428
                        a1 := start + float64(i)*da
×
429
                        a2 := a1 + da
×
430

×
431
                        x1, y1 := cosSinPoint(cx, cy, r, a1)
×
432
                        x2, y2 := cosSinPoint(cx, cy, r, a2)
×
433

×
434
                        k := 4.0 / 3.0 * math.Tan((a2-a1)/4.0)
×
435
                        // tangent unit vectors on our param (x = cx+rcos, y = cy-rsin)
×
436
                        c1x := x1 + k*r*(-math.Sin(a1))
×
437
                        c1y := y1 + k*r*(-math.Cos(a1))
×
438
                        c2x := x2 - k*r*(-math.Sin(a2))
×
439
                        c2y := y2 - k*r*(-math.Cos(a2))
×
440

×
441
                        adder.CubeBezier(
×
442
                                rasterx.ToFixedP(c1x, c1y),
×
443
                                rasterx.ToFixedP(c2x, c2y),
×
444
                                rasterx.ToFixedP(x2, y2),
×
445
                        )
×
446
                }
×
447
        }
448

449
        // full-circle/donut paths (two closed subpaths: outer CCW, inner CW if inner > 0)
450
        if math.Abs(sweep) >= 2*math.Pi {
×
451
                // outer loop (CCW)
×
452
                ox, oy := cosSinPoint(cx, cy, outer, 0)
×
453
                adder.Start(rasterx.ToFixedP(ox, oy))
×
454
                addCircularArc(adder, cx, cy, outer, 0, 2*math.Pi)
×
455
                adder.Stop(true)
×
456
                // inner loop reversed (CW) to create a hole
×
457
                if inner > 0 {
×
458
                        ix, iy := cosSinPoint(cx, cy, inner, 0)
×
459
                        adder.Start(rasterx.ToFixedP(ix, iy))
×
460
                        addCircularArc(adder, cx, cy, inner, 0, -2*math.Pi)
×
461
                        adder.Stop(true)
×
462
                }
×
463
                return
×
464
        }
465

466
        if cr <= 0 {
×
467
                // sharp-corner fallback
×
468
                if inner <= 0 {
×
469
                        // pie slice
×
470
                        ox, oy := cosSinPoint(cx, cy, outer, start)
×
471
                        adder.Start(rasterx.ToFixedP(cx, cy))
×
472
                        adder.Line(rasterx.ToFixedP(ox, oy))
×
473
                        addCircularArc(adder, cx, cy, outer, start, sweep)
×
474
                        adder.Line(rasterx.ToFixedP(cx, cy))
×
475
                        adder.Stop(true)
×
476
                        return
×
477
                }
×
478
                // annular sector
479
                outerStartX, outerStartY := cosSinPoint(cx, cy, outer, start)
×
480
                adder.Start(rasterx.ToFixedP(outerStartX, outerStartY))
×
481
                addCircularArc(adder, cx, cy, outer, start, sweep)
×
482
                innerEndX, innerEndY := cosSinPoint(cx, cy, inner, start+sweep)
×
483
                adder.Line(rasterx.ToFixedP(innerEndX, innerEndY))
×
484
                addCircularArc(adder, cx, cy, inner, start+sweep, -sweep)
×
485
                adder.Stop(true)
×
486
                return
×
487
        }
488

489
        // rounded corners
490
        sgn := 1.0
×
491
        if sweep < 0 {
×
492
                sgn = -1.0
×
493
        }
×
494
        absSweep := math.Abs(sweep)
×
495

×
496
        // clamp the corner radius if the value is too large
×
497
        cr = math.Min(cr, outer/2)
×
498

×
499
        // trim angles due to rounds
×
500
        sOut := math.Sqrt(math.Max(0, outer*(outer-2*cr)))
×
501
        thetaOut := math.Atan2(cr, sOut) // positive
×
502

×
503
        crIn := math.Min(cr, 0.5*math.Min(outer-inner, math.Abs(sweep)*inner))
×
504
        var sIn, thetaIn float64
×
505
        if inner > 0 {
×
506
                sIn = math.Sqrt(math.Max(0, inner*(inner+2*crIn)))
×
507
                thetaIn = math.Atan2(crIn, sIn)
×
508
        }
×
509

510
        // ensure the trim does not exceed half the sweep
511
        thetaOut = math.Min(thetaOut, absSweep/2.0-1e-6)
×
512
        if thetaOut < 0 {
×
513
                thetaOut = 0
×
514
        }
×
515
        if inner > 0 {
×
516
                thetaIn = math.Min(thetaIn, absSweep/2.0-1e-6)
×
517
                if thetaIn < 0 {
×
518
                        thetaIn = 0
×
519
                }
×
520
        }
521

522
        // trimmed arc angles
523
        startOuter := start + sgn*thetaOut
×
524
        endOuter := start + sweep - sgn*thetaOut
×
525

×
526
        startInner := 0.0
×
527
        endInner := 0.0
×
528
        if inner > 0 {
×
529
                startInner = start + sgn*thetaIn
×
530
                endInner = start + sweep - sgn*thetaIn
×
531
        }
×
532

533
        // direction frames at start/end radial lines
534
        // start side
535
        vSx, vSy := math.Cos(start), -math.Sin(start)
×
536
        tSx, tSy := -math.Sin(start), -math.Cos(start)
×
537
        nSx, nSy := sgn*tSx, sgn*tSy // interior side normal at start
×
538

×
539
        // end side
×
540
        endRad := start + sweep
×
541
        vEx, vEy := math.Cos(endRad), -math.Sin(endRad)
×
542
        tEx, tEy := -math.Sin(endRad), -math.Cos(endRad)
×
543
        nEx, nEy := -sgn*tEx, -sgn*tEy // interior side normal at end
×
544

×
545
        // key points on arcs
×
546
        pOutStartX, pOutStartY := cosSinPoint(cx, cy, outer, startOuter)
×
547
        pOutEndX, pOutEndY := cosSinPoint(cx, cy, outer, endOuter)
×
548

×
549
        var pInStartX, pInStartY, pInEndX, pInEndY float64
×
550
        if inner > 0 {
×
551
                pInStartX, pInStartY = cosSinPoint(cx, cy, inner, startInner)
×
552
                pInEndX, pInEndY = cosSinPoint(cx, cy, inner, endInner)
×
553
        }
×
554

555
        angleAt := func(cx, cy, x, y float64) float64 {
×
556
                return math.Atan2(cy-y, x-cx)
×
557
        }
×
558

559
        // round geometry at start/end
560
        // outer rounds
561
        aOutSx, aOutSy := cx+sOut*vSx, cy+sOut*vSy                      // radial tangent (start)
×
562
        fOutSx, fOutSy := aOutSx+cr*nSx, aOutSy+cr*nSy                  // round center (start)
×
563
        aOutEx, aOutEy := cx+sOut*vEx, cy+sOut*vEy                      // radial tangent (end)
×
564
        fOutEx, fOutEy := aOutEx+cr*nEx, aOutEy+cr*nEy                  // round center (end)
×
565
        phiOutEndB := angleAt(fOutEx, fOutEy, pOutEndX, pOutEndY)       // outer end trimmed point
×
566
        phiOutEndA := angleAt(fOutEx, fOutEy, aOutEx, aOutEy)           // end radial tangent
×
567
        phiOutStartA := angleAt(fOutSx, fOutSy, aOutSx, aOutSy)         // start radial tangent
×
568
        phiOutStartB := angleAt(fOutSx, fOutSy, pOutStartX, pOutStartY) // outer start trimmed point
×
569

×
570
        // inner rounds
×
571
        var aInSx, aInSy, fInSx, fInSy, aInEx, aInEy, fInEx, fInEy float64
×
572
        var phiInEndA, phiInEndB, phiInStartA, phiInStartB float64
×
573
        if inner > 0 {
×
574
                aInSx, aInSy = cx+sIn*vSx, cy+sIn*vSy
×
575
                fInSx, fInSy = aInSx+crIn*nSx, aInSy+crIn*nSy
×
576
                aInEx, aInEy = cx+sIn*vEx, cy+sIn*vEy
×
577
                fInEx, fInEy = aInEx+crIn*nEx, aInEy+crIn*nEy
×
578

×
579
                phiInEndA = angleAt(fInEx, fInEy, aInEx, aInEy)           // end radial tangent
×
580
                phiInEndB = angleAt(fInEx, fInEy, pInEndX, pInEndY)       // inner end trimmed point
×
581
                phiInStartB = angleAt(fInSx, fInSy, pInStartX, pInStartY) // inner start trimmed point
×
582
                phiInStartA = angleAt(fInSx, fInSy, aInSx, aInSy)         // start radial tangent
×
583
        }
×
584

585
        angleDiff := func(delta float64) float64 {
×
586
                return math.Atan2(math.Sin(delta), math.Cos(delta))
×
587
        }
×
588

589
        adder.Start(rasterx.ToFixedP(pOutStartX, pOutStartY))                                   // start at trimmed outer start
×
590
        addCircularArc(adder, cx, cy, outer, startOuter, endOuter-startOuter)                   // outer arc (trimmed)
×
591
        addCircularArc(adder, fOutEx, fOutEy, cr, phiOutEndB, angleDiff(phiOutEndA-phiOutEndB)) // end side: outer round to radial
×
592

×
593
        if inner > 0 {
×
594
                adder.Line(rasterx.ToFixedP(aInEx, aInEy))                                                 // end side: radial line to inner
×
595
                addCircularArc(adder, fInEx, fInEy, crIn, phiInEndA, angleDiff(phiInEndB-phiInEndA))       // end side: inner round to inner arc
×
596
                addCircularArc(adder, cx, cy, inner, endInner, startInner-endInner)                        // inner arc (reverse, trimmed)
×
597
                addCircularArc(adder, fInSx, fInSy, crIn, phiInStartB, angleDiff(phiInStartA-phiInStartB)) // start side: inner round to radial
×
598
                adder.Line(rasterx.ToFixedP(aOutSx, aOutSy))                                               // start side: radial line to outer
×
599
        } else {
×
600
                // pie slice: close via center with radial lines
×
601
                adder.Line(rasterx.ToFixedP(cx, cy))         // to center from end side
×
602
                adder.Line(rasterx.ToFixedP(aOutSx, aOutSy)) // to start-side radial tangent
×
603
        }
×
604

605
        // start side: outer round from radial to outer start
606
        addCircularArc(adder, fOutSx, fOutSy, cr, phiOutStartA, angleDiff(phiOutStartB-phiOutStartA))
×
607
        adder.Stop(true)
×
608
}
609

610
// GetCornerRadius returns the effective corner radius for a rectangle or square corner.
611
// If the specific corner radius (perCornerRadius) is zero, it falls back to the baseCornerRadius.
612
// Otherwise, it uses the specific corner radius provided.
613
//
614
// This allows for per-corner customization while maintaining a default overall radius.
615
func GetCornerRadius(perCornerRadius, baseCornerRadius float32) float32 {
4✔
616
        if perCornerRadius == 0.0 {
6✔
617
                return baseCornerRadius
2✔
618
        }
2✔
619
        return perCornerRadius
2✔
620
}
621

622
// GetMaximumRadius returns the maximum possible corner radius that fits within the given size.
623
// It calculates half of the smaller dimension (width or height) of the provided fyne.Size.
624
//
625
// This is typically used for drawing circular corners in rectangles, circles or squares with the same radius for all corners.
626
func GetMaximumRadius(size fyne.Size) float32 {
15✔
627
        return min(size.Height, size.Width) / 2
15✔
628
}
15✔
629

630
// GetMaximumCornerRadius returns the maximum possible corner radius for an individual corner,
631
// considering the specified corner radius, the radii of adjacent corners, and the maximum radii
632
// allowed for the width and height of the shape. Corner radius may utilize unused capacity from adjacent corners with radius smaller than maximum value
633
// so this corner can grow up to double the maximum radius of the smaller dimension (width or height) without causing overlaps.
634
//
635
// This is typically used for drawing circular corners in rectangles or squares with different corner radii.
636
func GetMaximumCornerRadius(radius, adjacentWidthRadius, adjacentHeightRadius float32, size fyne.Size) float32 {
18✔
637
        maxWidthRadius := size.Width / 2
18✔
638
        maxHeightRadius := size.Height / 2
18✔
639
        // fast path: corner radius fits within both per-axis maxima
18✔
640
        if radius <= min(maxWidthRadius, maxHeightRadius) {
22✔
641
                return radius
4✔
642
        }
4✔
643
        // expand per-axis limits by borrowing any unused capacity from adjacent corners
644
        expandedMaxWidthRadius := 2*maxWidthRadius - min(maxWidthRadius, adjacentWidthRadius)
14✔
645
        expandedMaxHeightRadius := 2*maxHeightRadius - min(maxHeightRadius, adjacentHeightRadius)
14✔
646

14✔
647
        // respect the smaller axis and never exceed the requested radius
14✔
648
        expandedMaxRadius := min(expandedMaxWidthRadius, expandedMaxHeightRadius)
14✔
649
        return min(expandedMaxRadius, radius)
14✔
650
}
651

652
// GetMaximumRadiusArc returns the maximum possible corner radius for an arc segment based on the outer radius,
653
// inner radius, and sweep angle in degrees.
654
// It calculates half of the smaller dimension (thickness or effective length) of the provided arc parameters
655
func GetMaximumRadiusArc(outerRadius, innerRadius, sweepAngle float32) float32 {
5✔
656
        // height (thickness), width (length)
5✔
657
        thickness := outerRadius - innerRadius
5✔
658
        // TODO: length formula can be improved to get a fully rounded (pill shape) outer edge for thin (small sweep) arc segments
5✔
659
        span := math.Sin(0.5 * math.Min(math.Abs(float64(sweepAngle))*math.Pi/180.0, math.Pi)) // span in (0,1)
5✔
660
        length := 1.5 * float64(outerRadius) * span / (1 + span)                               // no division-by-zero risk
5✔
661

5✔
662
        return GetMaximumRadius(fyne.NewSize(
5✔
663
                thickness, float32(length),
5✔
664
        ))
5✔
665
}
5✔
666

667
// NormalizeArcAngles adjusts the given start and end angles for arc drawing.
668
// It converts the angles from the Unit circle coordinate system (where 0 degrees is along the positive X-axis)
669
// to the coordinate system used by the painter, where 0 degrees is at the top (12 o'clock position).
670
// The function also reverses the direction: positive is clockwise, negative is counter-clockwise
671
func NormalizeArcAngles(startAngle, endAngle float32) (float32, float32) {
12✔
672
        return -(startAngle - 90), -(endAngle - 90)
12✔
673
}
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc